Patchwork [1,of,5] parsers: add a function to efficiently lowercase ASCII strings

login
register
mail settings
Submitter Siddharth Agarwal
Date Oct. 4, 2014, 3:23 a.m.
Message ID <cb91516e7745b936b8b7.1412393003@devbig136.prn2.facebook.com>
Download mbox | patch
Permalink /patch/6103/
State Superseded
Headers show

Comments

Siddharth Agarwal - Oct. 4, 2014, 3:23 a.m.
# HG changeset patch
# User Siddharth Agarwal <sid0@fb.com>
# Date 1412386959 25200
#      Fri Oct 03 18:42:39 2014 -0700
# Node ID cb91516e7745b936b8b70dbef1535cbc39d53277
# Parent  2805d23e1f885ff183b900e4407d3c31758d5b59
parsers: add a function to efficiently lowercase ASCII strings

We need a way to efficiently lowercase ASCII strings. For example, 'hg status'
needs to build up the fold map -- a map from a canonical case (for OS X,
lowercase) to the actual case of each file and directory in the dirstate.

The current way we do that is to try decoding to ASCII and then calling
lower() on the string, labeled 'orig' below:

    str.decode('ascii')
    return str.lower()

This is pretty inefficient, and it turns out we can do much better.

I also tested out a condition-based approach, labeled 'cond' below:

    (c >= 'A' && c <= 'Z') ? (c + ('a' - 'A')) : c

'cond' turned out to be slower in all cases. A 256-byte lookup table with
invalid values for everything past 127 performed similarly, but this was less
verbose.

On OS X 10.9 with LLVM version 6.0 (clang-600.0.51), the asciilower function
was run against two corpuses.

Corpus 1 (list of files from real-world repo, > 100k files):
orig:   wall 0.428567 comb 0.430000 user 0.430000 sys 0.000000 (best of 24)
cond:   wall 0.084710 comb 0.090000 user 0.090000 sys 0.000000 (best of 96)
lookup: wall 0.066813 comb 0.070000 user 0.070000 sys 0.000000 (best of 100)

Corpus 2 (mozilla-central, 113k files):
orig:   wall 0.238406 comb 0.240000 user 0.240000 sys 0.000000 (best of 42)
cond:   wall 0.044509 comb 0.040000 user 0.040000 sys 0.000000 (best of 100)
lookup: wall 0.040069 comb 0.040000 user 0.040000 sys 0.000000 (best of 100)

On a Linux server-class machine with GCC 4.4.6 20120305 (Red Hat 4.4.6-4):

Corpus 1 (real-world repo, > 100k files):
orig:   wall 0.260899 comb 0.260000 user 0.260000 sys 0.000000 (best of 38)
cond:   wall 0.060415 comb 0.060000 user 0.060000 sys 0.000000 (best of 100)
lookup: wall 0.054071 comb 0.060000 user 0.060000 sys 0.000000 (best of 100)

Corpus 2 (mozilla-central, 113k files):
orig:   wall 0.153082 comb 0.150000 user 0.150000 sys 0.000000 (best of 65)
cond:   wall 0.034163 comb 0.030000 user 0.030000 sys 0.000000 (best of 100)
lookup: wall 0.031084 comb 0.030000 user 0.030000 sys 0.000000 (best of 100)

SSE instructions might help even more, but I didn't experiment with those.
Antoine Pitrou - Oct. 4, 2014, 10:55 a.m.
On Fri, 3 Oct 2014 20:23:23 -0700
Siddharth Agarwal <sid0@fb.com> wrote:
> +static PyObject *asciilower(PyObject *self, PyObject *args)
> +{
> +	char *str, *newstr;
> +	int i, len;
> +	PyObject *newobj = NULL;
> +
> +	if (!PyArg_ParseTuple(args, "s#", &str, &len))
> +		goto quit;
> +
> +	newobj = PyBytes_FromStringAndSize(NULL, len);
> +	if (!newobj)
> +		goto quit;
> +
> +	newstr = PyBytes_AS_STRING(newobj);
> +
> +	Py_MEMCPY(newstr, str, len);

Why do you memcpy() first? You could read from str directly inside the
loop.

Regards

Antoine.
Siddharth Agarwal - Oct. 4, 2014, 7:01 p.m.
On 10/04/2014 03:55 AM, Antoine Pitrou wrote:
> Why do you memcpy() first? You could read from str directly inside the
> loop.

Leftover from an earlier attempt. Removing the memcpy results in a 
further 10% speedup. Nice. I'll resend with updated performance numbers.

- Siddharth


>
> Regards
>
> Antoine.
>
>
> _______________________________________________
> Mercurial-devel mailing list
> Mercurial-devel@selenic.com
> http://selenic.com/mailman/listinfo/mercurial-devel

Patch

diff --git a/mercurial/encoding.py b/mercurial/encoding.py
--- a/mercurial/encoding.py
+++ b/mercurial/encoding.py
@@ -5,7 +5,7 @@ 
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-import error
+import error, parsers
 import unicodedata, locale, os
 
 def _getpreferredencoding():
@@ -258,6 +258,20 @@ 
             return concat(usub.encode(encoding))
     return ellipsis # no enough room for multi-column characters
 
+def asciilower(s):
+    '''convert a string to lowercase if ASCII
+
+    Raises UnicodeDecodeError if non-ASCII characters are found.'''
+    s.decode('ascii')
+    return s.lower()
+
+# this would ideally be util.safehasattr, but making encoding depend on util is
+# a layering violation
+try:
+    asciilower = parsers.asciilower
+except AttributeError:
+    pass
+
 def lower(s):
     "best-effort encoding-aware case-folding of local string s"
     try:
diff --git a/mercurial/parsers.c b/mercurial/parsers.c
--- a/mercurial/parsers.c
+++ b/mercurial/parsers.c
@@ -35,6 +35,27 @@ 
 	-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1
 };
 
+static char lowertable[128] = {
+	'\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07',
+	'\x08', '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f',
+	'\x10', '\x11', '\x12', '\x13', '\x14', '\x15', '\x16', '\x17',
+	'\x18', '\x19', '\x1a', '\x1b', '\x1c', '\x1d', '\x1e', '\x1f',
+	'\x20', '\x21', '\x22', '\x23', '\x24', '\x25', '\x26', '\x27',
+	'\x28', '\x29', '\x2a', '\x2b', '\x2c', '\x2d', '\x2e', '\x2f',
+	'\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37',
+	'\x38', '\x39', '\x3a', '\x3b', '\x3c', '\x3d', '\x3e', '\x3f',
+	'\x40',
+	        '\x61', '\x62', '\x63', '\x64', '\x65', '\x66', '\x67', /* A-G */
+	'\x68', '\x69', '\x6a', '\x6b', '\x6c', '\x6d', '\x6e', '\x6f', /* H-O */
+	'\x70', '\x71', '\x72', '\x73', '\x74', '\x75', '\x76', '\x77', /* P-W */
+	'\x78', '\x79', '\x7a',                                         /* X-Z */
+	                        '\x5b', '\x5c', '\x5d', '\x5e', '\x5f',
+	'\x60', '\x61', '\x62', '\x63', '\x64', '\x65', '\x66', '\x67',
+	'\x68', '\x69', '\x6a', '\x6b', '\x6c', '\x6d', '\x6e', '\x6f',
+	'\x70', '\x71', '\x72', '\x73', '\x74', '\x75', '\x76', '\x77',
+	'\x78', '\x79', '\x7a', '\x7b', '\x7c', '\x7d', '\x7e', '\x7f',
+};
+
 static inline int hexdigit(const char *p, Py_ssize_t off)
 {
 	int8_t val = hextable[(unsigned char)p[off]];
@@ -72,6 +93,41 @@ 
 	return ret;
 }
 
+static PyObject *asciilower(PyObject *self, PyObject *args)
+{
+	char *str, *newstr;
+	int i, len;
+	PyObject *newobj = NULL;
+
+	if (!PyArg_ParseTuple(args, "s#", &str, &len))
+		goto quit;
+
+	newobj = PyBytes_FromStringAndSize(NULL, len);
+	if (!newobj)
+		goto quit;
+
+	newstr = PyBytes_AS_STRING(newobj);
+
+	Py_MEMCPY(newstr, str, len);
+
+	for (i = 0; i < len; i++) {
+		char c = newstr[i];
+		if (c & 0x80) {
+			PyObject *err = PyUnicodeDecodeError_Create(
+				"ascii", str, len, i, (i + 1),
+				"unexpected code byte");
+			PyErr_SetObject(PyExc_UnicodeDecodeError, err);
+			goto quit;
+		}
+		newstr[i] = lowertable[(unsigned char)c];
+	}
+
+	return newobj;
+quit:
+	Py_XDECREF(newobj);
+	return NULL;
+}
+
 /*
  * This code assumes that a manifest is stitched together with newline
  * ('\n') characters.
@@ -2165,6 +2221,7 @@ 
 	{"parse_manifest", parse_manifest, METH_VARARGS, "parse a manifest\n"},
 	{"parse_dirstate", parse_dirstate, METH_VARARGS, "parse a dirstate\n"},
 	{"parse_index2", parse_index2, METH_VARARGS, "parse a revlog index\n"},
+	{"asciilower", asciilower, METH_VARARGS, "lowercase if ASCII\n"},
 	{"encodedir", encodedir, METH_VARARGS, "encodedir a path\n"},
 	{"pathencode", pathencode, METH_VARARGS, "fncache-encode a path\n"},
 	{"lowerencode", lowerencode, METH_VARARGS, "lower-encode a path\n"},