You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
$ ./python -m timeit -s 'import re''re.compile("([^\ud800-\udbff]|\A)[\udc00-\udfff]([^\udc00-\udfff]|\Z)").search; re.purge()'
100 loops, best of 3: 4.16 msec per loop
$ ./python -m timeit -s 'import re''re.purge()''re.compile("[\udc80-\udcff]").search'
100 loops, best of 3: 5.72 msec per loop
$ ./python -m timeit 'h=lambda s, p=set(map(chr, range(0xDC80, 0xDCFF+1))): any(c in p for c in s)'
10000 loops, best of 3: 60.5 usec per loop
$ ./python -m timeit -s 'import re''re.purge()''re.compile("(?![^\udc80-\udcff])").search'
1000 loops, best of 3: 401 usec per loop
$ ./python -m timeit -s 'import re''re.purge()''re.compile("[^\udc80-\udcff]*\Z").match'
1000 loops, best of 3: 427 usec per loop
Runtime:
$ ./python -m timeit -s 'import re; h=re.compile("([^\ud800-\udbff]|\A)[\udc00-\udfff]([^\udc00-\udfff]|\Z)").search; s = "A"*1000''h(s)'
1000 loops, best of 3: 245 usec per loop
$ ./python -m timeit -s 'import re; h=re.compile("[\udc80-\udcff]").search; s = "A"*1000''h(s)'
10000 loops, best of 3: 30.1 usec per loop
$ ./python -m timeit -s 'h=lambda s, p=set(map(chr, range(0xDC80, 0xDCFF+1))): any(c in p for c in s); s = "A"*1000''h(s)'
10000 loops, best of 3: 164 usec per loop
$ ./python -m timeit -s 'import re; h=re.compile("(?![^\udc80-\udcff])").search; s = "A"*1000''h(s)'
10000 loops, best of 3: 98.3 usec per loop
$ ./python -m timeit -s 'import re; h=re.compile("[^\udc80-\udcff]*\Z").match; s = "A"*1000''h(s)'
10000 loops, best of 3: 34.6 usec per loop
$ ./python -m timeit -s 'h=lambda s, hn=set(map(chr, range(0xDC80, 0xDD00))).isdisjoint: not hn(s); s = "A"*1000''h(s)'
10000 loops, best of 3: 43.8 usec per loop
Testing runtime of the _has_surrogates functions
Generating chars...
Generating samples...
1.61 <- re.compile(current_regex).search
0.24 <- re.compile(short_regex).search
15.13 <- return any(c in surrogates for c in s)
10.21 <- for c in s: if c in surrogates: return True
0.85 <- return re.search(short_regex, s)
0.83 <- functools.partial(re.search, short_regex)
20.86 <- for c in map(ord, s): if c in range(0xDC80, 0xDCFF+1): return True
19.68 <- for c in map(ord, s): if 0xDC80 <= c <= 0xDCFF: return True
0.28 <- re.compile('[^\\udc80-\\udcff]*\Z').match
7.00 <- return not set(map(chr, range(0xDC80, 0xDCFF+1))).isdisjoint(s)
Testing startup time
0.57 <- r = re.compile('[\udc80-\udcff]').search
0.59 <- r = re.compile('[^\\udc80-\\udcff]\Z').match
199.79 <- r = re.compile('[\udc80-\udcff]').search; purge()
22.62 <- r = re.compile('[^\\udc80-\\udcff]\Z').match; purge()
1.12 <- r = pickle.loads(p)
Yes, however it has a startup cost that the function that returns re.search(short_regex, s) and the one with functool.partial don't have, because with these the compilation happens at the first call.
If we use one of these two, the startup time will be reduced a lot, and the runtime will be ~2x faster.
If we use re.compile(short_regex).search the startup time won't be reduced as much, but the runtime will be ~8x faster.
Given that here we are trying to reduce the startup time and not the runtime, I think using one of those two functions is better.
Another possible solution to improve the startup time is trying to optimize _optimize_unicode -- not sure how much can be done there though.
This issue may be about reducing the startup time, but this function is a hot spot in the email package so I would prefer to sacrifice startup time optimization for an increase in speed.
However, given the improvements to import locking in 3.3, what about a self replacing function?
but I have discarded it because it's not very pretty and still has the overhead of the function and an additional if. Your version solves both the problems in a more elegant way.
It would be better to add/improve the _has_surrogates tests before committing.
The patch I attached is also still valid if you want a further speed up improvement.
I'm really not willing to inline any of those pre-compiled regular expressions. They are precompiled because for a program processing lots of email, they are hot spots. We could use the same "compile on demand" dodge on them, though.
They are precompiled because for a program processing lots of email,
they are hot spots.
OK, I didn't know they were hot spots. Note that the regex are not recompiled everytime: they are compiled the first time and then taken from the cache (assuming they don't fall out from the bottom of the cache). This still has a small overhead though.
Can you explain your changes to the ecre regex (keeping in mind
that I don't know much about regex syntax).
- (?P<charset>[^?]*?) # non-greedy up to the next ? is the charset+ (?P<charset>[^?]*) # up to the next ? is the charset
\? # literal ?
(?P<encoding>[qb]) # either a "q" or a "b", case insensitive
\? # literal ?
- (?P<encoded>.*?) # non-greedy up to the next ?= is the encoded string+ (?P<encoded>[^?]*) # up to the next ?= is the encoded string
\?= # literal ?=
At the beginning, the non-greedy *? is unnecessary because [^?]* already stops at the first ? found.
The second change might actually be wrong if <encoded> is allowed to contain lone '?'s. The original regex used '.*?\?=', which means "match everything (including lone '?'s) until the first '?=')", mine means "match everything until the first '?'" which works fine as long as lone '?'s are not allowed.
Serhiy's suggestion is semantically different, but it might be still suitable if having _has_surrogate return True even for surrogates not in range \udc80-\udcff is OK.
Well, "other" surrogates will cause a different error later than with the current _has_surrogates logic, but it won't be any more mysterious than what would happen now, I think. Normally, if I understand correctly, other surrogates should never occur, so I don't think it is a real issue.
Yes, lone '?'s should not stop the pattern match in an encoded string. Even though I don't think they are normally supposed to occur, they do occur when encoded words are encoded incorrectly, and we get a better error recovery result if we look for ?= as the end.
I've checked in the encode version of the method. I'm going to pass on doing the other inlines, given that the improvement isn't that large. I will, however, keep the issue in mind as I make other changes to the code, and there will be a general performance review phase when I get done with the API additions/bug fixing in the email6 project.
Activity
serhiy-storchaka commentedon Sep 19, 2012
Startup-time:
Runtime:
serhiy-storchaka commentedon Sep 19, 2012
Faster set-version:
ezio-melotti commentedon Sep 19, 2012
Attached new benchmark file.
Results:
Testing runtime of the _has_surrogates functions
Generating chars...
Generating samples...
1.61 <- re.compile(current_regex).search
0.24 <- re.compile(short_regex).search
15.13 <- return any(c in surrogates for c in s)
10.21 <- for c in s: if c in surrogates: return True
0.85 <- return re.search(short_regex, s)
0.83 <- functools.partial(re.search, short_regex)
20.86 <- for c in map(ord, s): if c in range(0xDC80, 0xDCFF+1): return True
19.68 <- for c in map(ord, s): if 0xDC80 <= c <= 0xDCFF: return True
0.28 <- re.compile('[^\\udc80-\\udcff]*\Z').match
7.00 <- return not set(map(chr, range(0xDC80, 0xDCFF+1))).isdisjoint(s)
Testing startup time
0.57 <- r = re.compile('[\udc80-\udcff]').search
0.59 <- r = re.compile('[^\\udc80-\\udcff]\Z').match
199.79 <- r = re.compile('[\udc80-\udcff]').search; purge()
22.62 <- r = re.compile('[^\\udc80-\\udcff]\Z').match; purge()
1.12 <- r = pickle.loads(p)
bitdancer commentedon Sep 19, 2012
So by your measurements the short search is the clear winner?
ezio-melotti commentedon Sep 19, 2012
Yes, however it has a startup cost that the function that returns re.search(short_regex, s) and the one with functool.partial don't have, because with these the compilation happens at the first call.
If we use one of these two, the startup time will be reduced a lot, and the runtime will be ~2x faster.
If we use re.compile(short_regex).search the startup time won't be reduced as much, but the runtime will be ~8x faster.
Given that here we are trying to reduce the startup time and not the runtime, I think using one of those two functions is better.
Another possible solution to improve the startup time is trying to optimize _optimize_unicode -- not sure how much can be done there though.
bitdancer commentedon Sep 19, 2012
This issue may be about reducing the startup time, but this function is a hot spot in the email package so I would prefer to sacrifice startup time optimization for an increase in speed.
However, given the improvements to import locking in 3.3, what about a self replacing function?
ezio-melotti commentedon Sep 19, 2012
That might work.
To avoid the overhead of the cache lookup I was thinking about something like
but I have discarded it because it's not very pretty and still has the overhead of the function and an additional if. Your version solves both the problems in a more elegant way.
bitdancer commentedon Sep 19, 2012
It passed the email test suite. Patch attached.
ezio-melotti commentedon Sep 19, 2012
It would be better to add/improve the _has_surrogates tests before committing.
The patch I attached is also still valid if you want a further speed up improvement.
serhiy-storchaka commentedon Sep 19, 2012
Results:
0.26 <- re.compile(short_regex).search
0.06 <- try encode
bitdancer commentedon Sep 23, 2012
I'm really not willing to inline any of those pre-compiled regular expressions. They are precompiled because for a program processing lots of email, they are hot spots. We could use the same "compile on demand" dodge on them, though.
Can you explain your changes to the
bitdancer commentedon Sep 23, 2012
Woops.
Can you explain your changes to the ecre regex (keeping in mind that I don't know much about regex syntax).
bitdancer commentedon Sep 23, 2012
Oh, yeah, and the encode benchmark is very instructive, thanks Serhiy :)
ezio-melotti commentedon Sep 23, 2012
OK, I didn't know they were hot spots. Note that the regex are not recompiled everytime: they are compiled the first time and then taken from the cache (assuming they don't fall out from the bottom of the cache). This still has a small overhead though.
At the beginning, the non-greedy *? is unnecessary because [^?]* already stops at the first ? found.
The second change might actually be wrong if <encoded> is allowed to contain lone '?'s. The original regex used '.*?\?=', which means "match everything (including lone '?'s) until the first '?=')", mine means "match everything until the first '?'" which works fine as long as lone '?'s are not allowed.
Serhiy's suggestion is semantically different, but it might be still suitable if having _has_surrogate return True even for surrogates not in range \udc80-\udcff is OK.
bitdancer commentedon Sep 23, 2012
Well, "other" surrogates will cause a different error later than with the current _has_surrogates logic, but it won't be any more mysterious than what would happen now, I think. Normally, if I understand correctly, other surrogates should never occur, so I don't think it is a real issue.
Yes, lone '?'s should not stop the pattern match in an encoded string. Even though I don't think they are normally supposed to occur, they do occur when encoded words are encoded incorrectly, and we get a better error recovery result if we look for ?= as the end.
python-dev commentedon Jun 26, 2013
New changeset 520490c4c388 by R David Murray in branch 'default':
bpo-11454: Reduce email module load time, improve surrogate check efficiency.
http://hg.python.org/cpython/rev/520490c4c388
bitdancer commentedon Jun 26, 2013
I've checked in the encode version of the method. I'm going to pass on doing the other inlines, given that the improvement isn't that large. I will, however, keep the issue in mind as I make other changes to the code, and there will be a general performance review phase when I get done with the API additions/bug fixing in the email6 project.