GeezerVison is showing github.com — gh-122901: Speed up `str` creation from non-ASCII data with a branch-free `find_max_char()` by ruema · Pull Request #122902 · python/cpython
Conversation
ruema
commented
Aug 11, 2024
•
edited by bedevere-app
Bot
Loading
Uh oh!
There was an error while loading. Please reload this page.
Change to find_max_char to speedup string creation.
See issue #122901.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
This comment was marked as resolved.
StanFromIreland
changed the title
gh-122901: Improve performance of find_max_char
gh-122901: Speed up str creation from non-ASCII data with a branch-free find_max_char()
Oct 30, 2026
This comment was marked as resolved.
|
I did some benchmarking with the following script:
Details
import pyperf
LATIN1 = '\xe9'
UCS2 = '\u019f'
UCS4 = '\U0001f600'
def slice_tail(s):
return s[:-1]
def slice_head(s):
return s[1:]
def slice_head3(s):
return s[3:]
def isascii(b):
return b.isascii()
def decode_latin1(b):
return b.decode('latin-1')
def split_lines(s):
return s.split('\n')
CASES = {
'control: ascii[1001] slice[:-1]': (slice_tail, 'a' * 1001),
'ucs1[8] slice[:-1]': (slice_tail, 'a' * 7 + LATIN1),
'ucs1[21] slice[:-1]': (slice_tail, 'a' * 20 + LATIN1),
'ucs1[1001] slice[:-1]': (slice_tail, 'a' * 1000 + LATIN1),
'ucs1[1001] slice[1:] (odd ptr)': (slice_head, LATIN1 + 'a' * 1000),
'ucs1[1001] slice[3:]': (slice_head3, LATIN1 * 3 + 'a' * 998),
'ucs1[1001] non-ascii body slice[:-1]': (slice_tail, LATIN1 * 1000 + 'a'),
'ucs1[100001] slice[:-1]': (slice_tail, 'a' * 100000 + LATIN1),
'ucs1[10_000_001] slice[:-1]': (slice_tail, 'a' * 10_000_000 + LATIN1),
'ucs2[21] slice[:-1]': (slice_tail, 'a' * 20 + UCS2),
'ucs2[1002] slice[:-1] (issue)': (slice_tail, 'a' * 1000 + UCS2 * 2),
'ucs2[1001] latin1 body slice[:-1]': (slice_tail, LATIN1 * 1000 + UCS2),
'ucs2[1001] slice[1:]': (slice_head, UCS2 + 'a' * 1000),
'ucs2[1001] wide body slice[:-1]': (slice_tail, UCS2 * 1000 + 'a'),
'ucs4[21] slice[:-1]': (slice_tail, 'a' * 20 + UCS4),
'ucs4[1001] slice[:-1]': (slice_tail, 'a' * 1000 + UCS4),
'ucs4[1001] wide body slice[:-1]': (slice_tail, UCS4 * 1000 + 'a'),
'bytes[10] isascii': (isascii, b'a' * 10),
'bytes[1000] isascii': (isascii, b'a' * 1000),
'bytes[1000] decode latin-1': (decode_latin1, b'a' * 1000),
'bytes[1000] non-ascii decode latin-1': (decode_latin1, b'\xe9' * 1000),
'ucs1 200 lines x 51 split': (split_lines, ('abcdefghij' * 5 + LATIN1 + '\n') * 200),
'ucs1 200 lines x 11 split': (split_lines, ('abcdefghij' + LATIN1 + '\n') * 200),
'ucs2 200 lines x 51 split': (split_lines, ('abcdefghij' * 5 + UCS2 + '\n') * 200),
}
runner = pyperf.Runner()
for name, (func, arg) in CASES.items():
runner.bench_func(name, func, arg)
AMD Ryzen 7 8845HS, --enable-optimizations & --with-lto, GCC 15.3.1, Linux-7.1.13-100
control: ascii[1001] slice[:-1]: Mean +- std dev: [maxchar-main] 85.1 ns +- 4.8 ns -> [maxchar-pr] 88.5 ns +- 5.0 ns: 1.04x slower
ucs1[8] slice[:-1]: Mean +- std dev: [maxchar-main] 45.3 ns +- 2.9 ns -> [maxchar-pr] 43.8 ns +- 2.7 ns: 1.03x faster
ucs1[21] slice[:-1]: Mean +- std dev: [maxchar-main] 48.0 ns +- 5.5 ns -> [maxchar-pr] 46.3 ns +- 2.2 ns: 1.04x faster
ucs1[1001] slice[:-1]: Mean +- std dev: [maxchar-main] 120 ns +- 5 ns -> [maxchar-pr] 103 ns +- 6 ns: 1.17x faster
ucs1[1001] slice[1:] (odd ptr): Mean +- std dev: [maxchar-main] 124 ns +- 5 ns -> [maxchar-pr] 108 ns +- 3 ns: 1.14x faster
ucs1[1001] slice[3:]: Mean +- std dev: [maxchar-main] 124 ns +- 4 ns -> [maxchar-pr] 111 ns +- 5 ns: 1.11x faster
ucs1[1001] non-ascii body slice[:-1]: Mean +- std dev: [maxchar-main] 73.6 ns +- 3.9 ns -> [maxchar-pr] 83.4 ns +- 4.3 ns: 1.13x slower
ucs1[100001] slice[:-1]: Mean +- std dev: [maxchar-main] 6.71 us +- 0.22 us -> [maxchar-pr] 4.47 us +- 0.14 us: 1.50x faster
ucs1[10_000_001] slice[:-1]: Mean +- std dev: [maxchar-main] 788 us +- 34 us -> [maxchar-pr] 520 us +- 4 us: 1.52x faster
ucs2[21] slice[:-1]: Mean +- std dev: [maxchar-main] 48.4 ns +- 4.2 ns -> [maxchar-pr] 45.0 ns +- 1.8 ns: 1.08x faster
ucs2[1002] slice[:-1] (issue): Mean +- std dev: [maxchar-main] 195 ns +- 7 ns -> [maxchar-pr] 136 ns +- 2 ns: 1.44x faster
ucs2[1001] latin1 body slice[:-1]: Mean +- std dev: [maxchar-main] 195 ns +- 5 ns -> [maxchar-pr] 137 ns +- 3 ns: 1.42x faster
ucs2[1001] slice[1:]: Mean +- std dev: [maxchar-main] 195 ns +- 5 ns -> [maxchar-pr] 139 ns +- 2 ns: 1.40x faster
ucs4[21] slice[:-1]: Mean +- std dev: [maxchar-main] 50.7 ns +- 2.1 ns -> [maxchar-pr] 49.9 ns +- 1.9 ns: 1.01x faster
ucs4[1001] slice[:-1]: Mean +- std dev: [maxchar-main] 309 ns +- 8 ns -> [maxchar-pr] 331 ns +- 11 ns: 1.07x slower
ucs4[1001] wide body slice[:-1]: Mean +- std dev: [maxchar-main] 101 ns +- 4 ns -> [maxchar-pr] 106 ns +- 2 ns: 1.04x slower
bytes[1000] isascii: Mean +- std dev: [maxchar-main] 61.5 ns +- 2.2 ns -> [maxchar-pr] 84.0 ns +- 2.9 ns: 1.36x slower
bytes[1000] decode latin-1: Mean +- std dev: [maxchar-main] 134 ns +- 4 ns -> [maxchar-pr] 119 ns +- 8 ns: 1.12x faster
bytes[1000] non-ascii decode latin-1: Mean +- std dev: [maxchar-main] 82.7 ns +- 4.6 ns -> [maxchar-pr] 96.6 ns +- 5.6 ns: 1.17x slower
ucs1 200 lines x 51 split: Mean +- std dev: [maxchar-main] 5.69 us +- 0.17 us -> [maxchar-pr] 5.79 us +- 0.09 us: 1.02x slower
ucs1 200 lines x 11 split: Mean +- std dev: [maxchar-main] 3.67 us +- 0.14 us -> [maxchar-pr] 3.45 us +- 0.05 us: 1.06x faster
ucs2 200 lines x 51 split: Mean +- std dev: [maxchar-main] 8.57 us +- 0.17 us -> [maxchar-pr] 7.92 us +- 0.12 us: 1.08x faster
Benchmark hidden because not significant (2): ucs2[1001] wide body slice[:-1], bytes[10] isascii
Geometric mean: 1.08x faster
Intel(R) Core(TM) i5-7500T CPU @ 2.70GHz, release-build, GCC 15.2.1, Linux-6.14.0-63
control: ascii[1001] slice[:-1]: Mean +- std dev: [maxchar-main] 228 ns +- 13 ns -> [maxchar-pr] 220 ns +- 7 ns: 1.04x faster
ucs1[8] slice[:-1]: Mean +- std dev: [maxchar-main] 143 ns +- 1 ns -> [maxchar-pr] 140 ns +- 1 ns: 1.02x faster
ucs1[21] slice[:-1]: Mean +- std dev: [maxchar-main] 142 ns +- 4 ns -> [maxchar-pr] 141 ns +- 1 ns: 1.01x faster
ucs1[1001] slice[:-1]: Mean +- std dev: [maxchar-main] 286 ns +- 2 ns -> [maxchar-pr] 248 ns +- 5 ns: 1.16x faster
ucs1[1001] slice[1:] (odd ptr): Mean +- std dev: [maxchar-main] 295 ns +- 4 ns -> [maxchar-pr] 251 ns +- 4 ns: 1.18x faster
ucs1[1001] slice[3:]: Mean +- std dev: [maxchar-main] 293 ns +- 3 ns -> [maxchar-pr] 249 ns +- 2 ns: 1.18x faster
ucs1[1001] non-ascii body slice[:-1]: Mean +- std dev: [maxchar-main] 227 ns +- 1 ns -> [maxchar-pr] 232 ns +- 2 ns: 1.02x slower
ucs1[100001] slice[:-1]: Mean +- std dev: [maxchar-main] 7.60 us +- 0.14 us -> [maxchar-pr] 4.75 us +- 0.11 us: 1.60x faster
ucs1[10_000_001] slice[:-1]: Mean +- std dev: [maxchar-main] 1.24 ms +- 0.01 ms -> [maxchar-pr] 969 us +- 10 us: 1.28x faster
ucs2[21] slice[:-1]: Mean +- std dev: [maxchar-main] 145 ns +- 1 ns -> [maxchar-pr] 144 ns +- 1 ns: 1.01x faster
ucs2[1002] slice[:-1] (issue): Mean +- std dev: [maxchar-main] 483 ns +- 6 ns -> [maxchar-pr] 272 ns +- 1 ns: 1.77x faster
ucs2[1001] latin1 body slice[:-1]: Mean +- std dev: [maxchar-main] 521 ns +- 2 ns -> [maxchar-pr] 295 ns +- 4 ns: 1.77x faster
ucs2[1001] slice[1:]: Mean +- std dev: [maxchar-main] 526 ns +- 3 ns -> [maxchar-pr] 297 ns +- 2 ns: 1.77x faster
ucs2[1001] wide body slice[:-1]: Mean +- std dev: [maxchar-main] 239 ns +- 2 ns -> [maxchar-pr] 241 ns +- 2 ns: 1.01x slower
ucs4[21] slice[:-1]: Mean +- std dev: [maxchar-main] 148 ns +- 1 ns -> [maxchar-pr] 147 ns +- 1 ns: 1.01x faster
ucs4[1001] slice[:-1]: Mean +- std dev: [maxchar-main] 808 ns +- 6 ns -> [maxchar-pr] 475 ns +- 4 ns: 1.70x faster
bytes[10] isascii: Mean +- std dev: [maxchar-main] 92.9 ns +- 0.5 ns -> [maxchar-pr] 92.7 ns +- 0.4 ns: 1.00x faster
bytes[1000] isascii: Mean +- std dev: [maxchar-main] 151 ns +- 2 ns -> [maxchar-pr] 108 ns +- 1 ns: 1.39x faster
bytes[1000] decode latin-1: Mean +- std dev: [maxchar-main] 308 ns +- 2 ns -> [maxchar-pr] 267 ns +- 2 ns: 1.16x faster
bytes[1000] non-ascii decode latin-1: Mean +- std dev: [maxchar-main] 256 ns +- 8 ns -> [maxchar-pr] 250 ns +- 2 ns: 1.02x faster
ucs1 200 lines x 51 split: Mean +- std dev: [maxchar-main] 15.0 us +- 0.2 us -> [maxchar-pr] 12.1 us +- 0.6 us: 1.24x faster
ucs1 200 lines x 11 split: Mean +- std dev: [maxchar-main] 9.74 us +- 0.21 us -> [maxchar-pr] 8.39 us +- 0.18 us: 1.16x faster
ucs2 200 lines x 51 split: Mean +- std dev: [maxchar-main] 17.2 us +- 1.2 us -> [maxchar-pr] 12.1 us +- 0.7 us: 1.43x faster
Benchmark hidden because not significant (1): ucs4[1001] wide body slice[:-1]
Geometric mean: 1.22x faster
The results are quite compiler and build specific. Disassembling the PGO+LTO binary shows that GCC compiled the 32-word loop as a plain scalar loop rather than vectorising it, presumably because the loop is not hot enough in the PGO training run, and in that case the single accumulator becomes a serial dependency chain, which explains the slowdowns on bytes.isascii() and the UCS4 slice. I'll try to adjust the loop so it stays fast whether or not it gets vectorised, without making this too complex...
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
So, after some testing I pushed a little change to use four accumulators and a 16-word block so the loop is faster even when it's not vectorised. Benchmarking again, some more promising results:
AMD Ryzen 7 8845HS, --enable-optimizations & --with-lto, GCC 15.3.1, Linux-7.1.13-100
control: ascii[1001] slice[:-1]: Mean +- std dev: [maxchar-main] 85.1 ns +- 4.8 ns -> [maxchar-pr-4acc] 107 ns +- 4 ns: 1.26x slower
ucs1[8] slice[:-1]: Mean +- std dev: [maxchar-main] 45.3 ns +- 2.9 ns -> [maxchar-pr-4acc] 44.4 ns +- 2.8 ns: 1.02x faster
ucs1[21] slice[:-1]: Mean +- std dev: [maxchar-main] 48.0 ns +- 5.5 ns -> [maxchar-pr-4acc] 46.3 ns +- 1.9 ns: 1.04x faster
ucs1[1001] slice[:-1]: Mean +- std dev: [maxchar-main] 120 ns +- 5 ns -> [maxchar-pr-4acc] 84.2 ns +- 3.0 ns: 1.42x faster
ucs1[1001] slice[1:] (odd ptr): Mean +- std dev: [maxchar-main] 124 ns +- 5 ns -> [maxchar-pr-4acc] 88.0 ns +- 2.1 ns: 1.41x faster
ucs1[1001] slice[3:]: Mean +- std dev: [maxchar-main] 124 ns +- 4 ns -> [maxchar-pr-4acc] 91.4 ns +- 4.4 ns: 1.36x faster
ucs1[100001] slice[:-1]: Mean +- std dev: [maxchar-main] 6.71 us +- 0.22 us -> [maxchar-pr-4acc] 2.60 us +- 0.16 us: 2.59x faster
ucs1[10_000_001] slice[:-1]: Mean +- std dev: [maxchar-main] 788 us +- 34 us -> [maxchar-pr-4acc] 319 us +- 39 us: 2.47x faster
ucs2[21] slice[:-1]: Mean +- std dev: [maxchar-main] 48.4 ns +- 4.2 ns -> [maxchar-pr-4acc] 47.2 ns +- 2.9 ns: 1.03x faster
ucs2[1002] slice[:-1] (issue): Mean +- std dev: [maxchar-main] 195 ns +- 7 ns -> [maxchar-pr-4acc] 114 ns +- 4 ns: 1.72x faster
ucs2[1001] latin1 body slice[:-1]: Mean +- std dev: [maxchar-main] 195 ns +- 5 ns -> [maxchar-pr-4acc] 115 ns +- 3 ns: 1.70x faster
ucs2[1001] slice[1:]: Mean +- std dev: [maxchar-main] 195 ns +- 5 ns -> [maxchar-pr-4acc] 117 ns +- 4 ns: 1.67x faster
ucs2[1001] wide body slice[:-1]: Mean +- std dev: [maxchar-main] 91.5 ns +- 8.0 ns -> [maxchar-pr-4acc] 87.2 ns +- 4.8 ns: 1.05x faster
ucs4[21] slice[:-1]: Mean +- std dev: [maxchar-main] 50.7 ns +- 2.1 ns -> [maxchar-pr-4acc] 48.2 ns +- 3.0 ns: 1.05x faster
ucs4[1001] slice[:-1]: Mean +- std dev: [maxchar-main] 309 ns +- 8 ns -> [maxchar-pr-4acc] 256 ns +- 9 ns: 1.21x faster
ucs4[1001] wide body slice[:-1]: Mean +- std dev: [maxchar-main] 101 ns +- 4 ns -> [maxchar-pr-4acc] 95.1 ns +- 2.6 ns: 1.07x faster
bytes[10] isascii: Mean +- std dev: [maxchar-main] 36.4 ns +- 1.8 ns -> [maxchar-pr-4acc] 35.9 ns +- 1.4 ns: 1.01x faster
bytes[1000] isascii: Mean +- std dev: [maxchar-main] 61.5 ns +- 2.2 ns -> [maxchar-pr-4acc] 48.6 ns +- 1.7 ns: 1.27x faster
bytes[1000] decode latin-1: Mean +- std dev: [maxchar-main] 134 ns +- 4 ns -> [maxchar-pr-4acc] 91.4 ns +- 2.5 ns: 1.46x faster
bytes[1000] non-ascii decode latin-1: Mean +- std dev: [maxchar-main] 82.7 ns +- 4.6 ns -> [maxchar-pr-4acc] 80.7 ns +- 2.0 ns: 1.02x faster
ucs1 200 lines x 51 split: Mean +- std dev: [maxchar-main] 5.69 us +- 0.17 us -> [maxchar-pr-4acc] 5.61 us +- 0.15 us: 1.01x faster
ucs1 200 lines x 11 split: Mean +- std dev: [maxchar-main] 3.67 us +- 0.14 us -> [maxchar-pr-4acc] 3.39 us +- 0.11 us: 1.08x faster
ucs2 200 lines x 51 split: Mean +- std dev: [maxchar-main] 8.57 us +- 0.17 us -> [maxchar-pr-4acc] 7.39 us +- 0.16 us: 1.16x faster
Benchmark hidden because not significant (1): ucs1[1001] non-ascii body slice[:-1]
Geometric mean: 1.26x faster
Intel(R) Core(TM) i5-7500T CPU @ 2.70GHz, release-build, GCC 15.2.1, Linux-6.14.0-63
control: ascii[1001] slice[:-1]: Mean +- std dev: [maxchar-main] 228 ns +- 13 ns -> [maxchar-pr-4acc] 220 ns +- 8 ns: 1.04x faster
ucs1[8] slice[:-1]: Mean +- std dev: [maxchar-main] 143 ns +- 1 ns -> [maxchar-pr-4acc] 139 ns +- 2 ns: 1.03x faster
ucs1[21] slice[:-1]: Mean +- std dev: [maxchar-main] 142 ns +- 4 ns -> [maxchar-pr-4acc] 141 ns +- 1 ns: 1.01x faster
ucs1[1001] slice[:-1]: Mean +- std dev: [maxchar-main] 286 ns +- 2 ns -> [maxchar-pr-4acc] 251 ns +- 2 ns: 1.14x faster
ucs1[1001] slice[1:] (odd ptr): Mean +- std dev: [maxchar-main] 295 ns +- 4 ns -> [maxchar-pr-4acc] 253 ns +- 2 ns: 1.17x faster
ucs1[1001] slice[3:]: Mean +- std dev: [maxchar-main] 293 ns +- 3 ns -> [maxchar-pr-4acc] 252 ns +- 4 ns: 1.16x faster
ucs1[1001] non-ascii body slice[:-1]: Mean +- std dev: [maxchar-main] 227 ns +- 1 ns -> [maxchar-pr-4acc] 230 ns +- 2 ns: 1.02x slower
ucs1[100001] slice[:-1]: Mean +- std dev: [maxchar-main] 7.60 us +- 0.14 us -> [maxchar-pr-4acc] 6.37 us +- 0.09 us: 1.19x faster
ucs1[10_000_001] slice[:-1]: Mean +- std dev: [maxchar-main] 1.24 ms +- 0.01 ms -> [maxchar-pr-4acc] 1.09 ms +- 0.01 ms: 1.13x faster
ucs2[21] slice[:-1]: Mean +- std dev: [maxchar-main] 145 ns +- 1 ns -> [maxchar-pr-4acc] 145 ns +- 1 ns: 1.00x faster
ucs2[1002] slice[:-1] (issue): Mean +- std dev: [maxchar-main] 483 ns +- 6 ns -> [maxchar-pr-4acc] 296 ns +- 6 ns: 1.63x faster
ucs2[1001] latin1 body slice[:-1]: Mean +- std dev: [maxchar-main] 521 ns +- 2 ns -> [maxchar-pr-4acc] 336 ns +- 4 ns: 1.55x faster
ucs2[1001] slice[1:]: Mean +- std dev: [maxchar-main] 526 ns +- 3 ns -> [maxchar-pr-4acc] 338 ns +- 4 ns: 1.56x faster
ucs4[21] slice[:-1]: Mean +- std dev: [maxchar-main] 148 ns +- 1 ns -> [maxchar-pr-4acc] 147 ns +- 1 ns: 1.01x faster
ucs4[1001] slice[:-1]: Mean +- std dev: [maxchar-main] 808 ns +- 6 ns -> [maxchar-pr-4acc] 582 ns +- 31 ns: 1.39x faster
ucs4[1001] wide body slice[:-1]: Mean +- std dev: [maxchar-main] 257 ns +- 6 ns -> [maxchar-pr-4acc] 259 ns +- 8 ns: 1.01x slower
bytes[10] isascii: Mean +- std dev: [maxchar-main] 92.9 ns +- 0.5 ns -> [maxchar-pr-4acc] 95.4 ns +- 0.4 ns: 1.03x slower
bytes[1000] isascii: Mean +- std dev: [maxchar-main] 151 ns +- 2 ns -> [maxchar-pr-4acc] 119 ns +- 1 ns: 1.27x faster
bytes[1000] decode latin-1: Mean +- std dev: [maxchar-main] 308 ns +- 2 ns -> [maxchar-pr-4acc] 270 ns +- 4 ns: 1.14x faster
bytes[1000] non-ascii decode latin-1: Mean +- std dev: [maxchar-main] 256 ns +- 8 ns -> [maxchar-pr-4acc] 251 ns +- 8 ns: 1.02x faster
ucs1 200 lines x 51 split: Mean +- std dev: [maxchar-main] 15.0 us +- 0.2 us -> [maxchar-pr-4acc] 14.5 us +- 0.4 us: 1.04x faster
ucs1 200 lines x 11 split: Mean +- std dev: [maxchar-main] 9.74 us +- 0.21 us -> [maxchar-pr-4acc] 8.89 us +- 0.16 us: 1.10x faster
ucs2 200 lines x 51 split: Mean +- std dev: [maxchar-main] 17.2 us +- 1.2 us -> [maxchar-pr-4acc] 14.8 us +- 0.2 us: 1.16x faster
Benchmark hidden because not significant (1): ucs2[1001] wide body slice[:-1]
Geometric mean: 1.14x faster
The slowdown in the control in the PGO+LTO build is, as far as I can tell, just a build loosing in the PGO lottery. As expected in the release build, there should be no difference other than noise.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
eendebakpt
commented
Oct 30, 2026
•
edited by StanFromIreland
Loading
Uh oh!
There was an error while loading. Please reload this page.
|
The general idea is good, but there are quite some variations possible. The latest commit (small workaround for the compiler not vectorizing) is not great.
I tried prompting Claude to come up with simpler approaches. One result is here: pr122902..find-max-char-reduce. Less code, easier to read (imo) and a bit faster (on my noisy system).
Benchmark details
| Benchmark |
main |
PR 122902 |
find-max-char-reduce |
| control: ascii[1001] slice[:-1] |
194 ns |
not significant |
212 ns: 1.09x slower |
| ucs1[8] slice[:-1] |
160 ns |
170 ns: 1.06x slower |
not significant |
| ucs1[21] slice[:-1] |
160 ns |
not significant |
177 ns: 1.10x slower |
| ucs1[1001] slice[:-1] |
239 ns |
258 ns: 1.08x slower |
not significant |
| ucs1[1001] slice[1:] (odd ptr) |
|
not significant |
not significant |
| ucs1[1001] slice[3:] |
|
not significant |
not significant |
| ucs1[1001] non-ascii body slice[:-1] |
189 ns |
211 ns: 1.12x slower |
200 ns: 1.06x slower |
| ucs1[100001] slice[:-1] |
4.98 us |
4.57 us: 1.09x faster |
4.01 us: 1.24x faster |
| ucs1[10_000_001] slice[:-1] |
1.98 ms |
2.46 ms: 1.24x slower |
not significant |
| ucs2[21] slice[:-1] |
150 ns |
157 ns: 1.04x slower |
not significant |
| ucs2[1002] slice[:-1] (issue) |
405 ns |
256 ns: 1.58x faster |
251 ns: 1.62x faster |
| ucs2[1001] latin1 body slice[:-1] |
566 ns |
430 ns: 1.32x faster |
438 ns: 1.29x faster |
| ucs2[1001] slice[1:] |
627 ns |
456 ns: 1.38x faster |
445 ns: 1.41x faster |
| ucs2[1001] wide body slice[:-1] |
|
not significant |
not significant |
| ucs4[21] slice[:-1] |
156 ns |
168 ns: 1.07x slower |
not significant |
| ucs4[1001] slice[:-1] |
528 ns |
482 ns: 1.09x faster |
484 ns: 1.09x faster |
| ucs4[1001] wide body slice[:-1] |
216 ns |
229 ns: 1.06x slower |
223 ns: 1.03x slower |
| bytes[10] isascii |
|
not significant |
not significant |
| bytes[1000] isascii |
173 ns |
not significant |
138 ns: 1.25x faster |
| bytes[1000] decode latin-1 |
298 ns |
326 ns: 1.09x slower |
272 ns: 1.10x faster |
| bytes[1000] non-ascii decode latin-1 |
286 ns |
258 ns: 1.11x faster |
246 ns: 1.16x faster |
| ucs1 200 lines x 51 split |
10.2 us |
9.19 us: 1.10x faster |
9.56 us: 1.06x faster |
| ucs1 200 lines x 11 split |
7.17 us |
6.34 us: 1.13x faster |
6.62 us: 1.08x faster |
| ucs2 200 lines x 51 split |
11.6 us |
9.11 us: 1.27x faster |
9.14 us: 1.27x faster |
| Geometric mean |
(ref) |
1.04x faster |
1.08x faster |
Edited by Stan to fix table not rendering.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
Hmm, what build were the benchmarks run on? I'll test it.
What I'm curious is what happens if the vectorizer does not run?
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
Benchmarking your branch @eendebakpt, as I feared, on an LTO+PGO build:
| Benchmark |
maxchar-main |
maxchar-pr-4acc |
maxchar-pieter |
| control: ascii[1001] slice[:-1] |
85.1 ns |
107 ns: 1.26x slower |
91.4 ns: 1.07x slower |
| ucs1[8] slice[:-1] |
45.3 ns |
44.4 ns: 1.02x faster |
44.5 ns: 1.02x faster |
| ucs1[21] slice[:-1] |
48.0 ns |
46.3 ns: 1.04x faster |
not significant |
| ucs1[1001] slice[:-1] |
120 ns |
84.2 ns: 1.42x faster |
108 ns: 1.11x faster |
| ucs1[1001] slice[1:] (odd ptr) |
124 ns |
88.0 ns: 1.41x faster |
112 ns: 1.10x faster |
| ucs1[1001] slice[3:] |
124 ns |
91.4 ns: 1.36x faster |
112 ns: 1.11x faster |
| ucs1[1001] non-ascii body slice[:-1] |
73.6 ns |
not significant |
79.4 ns: 1.08x slower |
| ucs1[100001] slice[:-1] |
6.71 us |
2.60 us: 2.59x faster |
4.84 us: 1.39x faster |
| ucs1[10_000_001] slice[:-1] |
788 us |
319 us: 2.47x faster |
585 us: 1.35x faster |
| ucs2[21] slice[:-1] |
48.4 ns |
47.2 ns: 1.03x faster |
not significant |
| ucs2[1002] slice[:-1] (issue) |
195 ns |
114 ns: 1.72x faster |
112 ns: 1.74x faster |
| ucs2[1001] latin1 body slice[:-1] |
195 ns |
115 ns: 1.70x faster |
115 ns: 1.70x faster |
| ucs2[1001] slice[1:] |
195 ns |
117 ns: 1.67x faster |
119 ns: 1.64x faster |
| ucs2[1001] wide body slice[:-1] |
91.5 ns |
87.2 ns: 1.05x faster |
88.2 ns: 1.04x faster |
| ucs4[21] slice[:-1] |
50.7 ns |
48.2 ns: 1.05x faster |
52.6 ns: 1.04x slower |
| ucs4[1001] slice[:-1] |
309 ns |
256 ns: 1.21x faster |
270 ns: 1.15x faster |
| ucs4[1001] wide body slice[:-1] |
101 ns |
95.1 ns: 1.07x faster |
not significant |
| bytes[10] isascii |
36.4 ns |
35.9 ns: 1.01x faster |
38.5 ns: 1.06x slower |
| bytes[1000] isascii |
61.5 ns |
48.6 ns: 1.27x faster |
269 ns: 4.37x slower |
| bytes[1000] decode latin-1 |
134 ns |
91.4 ns: 1.46x faster |
117 ns: 1.14x faster |
| bytes[1000] non-ascii decode latin-1 |
82.7 ns |
80.7 ns: 1.02x faster |
91.3 ns: 1.10x slower |
| ucs1 200 lines x 51 split |
5.69 us |
5.61 us: 1.01x faster |
5.62 us: 1.01x faster |
| ucs1 200 lines x 11 split |
3.67 us |
3.39 us: 1.08x faster |
3.41 us: 1.08x faster |
| ucs2 200 lines x 51 split |
8.57 us |
7.39 us: 1.16x faster |
9.60 us: 1.12x slower |
| Geometric mean |
(ref) |
1.26x faster |
1.04x faster |
And indeed, it was not vectorised, looking at the disassembly:
0000000000621710 <stringlib_find_max_char.lto_priv.0>:
621710: xor %eax,%eax
621712: mov %rsi,%rdx
621715: sub %rdi,%rdx
621718: cmp $0x3f,%rdx
62171c: jle 621727 <stringlib_find_max_char.lto_priv.0+0x17>
62171e: jmp 62173c <stringlib_find_max_char.lto_priv.0+0x2c>
621720: add $0x1,%rdi
621724: or -0x1(%rdi),%al
621727: cmp %rsi,%rdi
62172a: jb 621720 <stringlib_find_max_char.lto_priv.0+0x10>
62172c: test %al,%al
62172e: mov $0xff,%edx
621733: mov $0x7f,%eax
621738: cmovs %edx,%eax
62173b: ret
62173c: xor %edx,%edx
62173e: xor %ecx,%ecx
621740: or (%rdi,%rdx,1),%cl
621743: add $0x1,%rdx
621747: cmp $0x40,%rdx
62174b: jne 621740 <stringlib_find_max_char.lto_priv.0+0x30>
62174d: or %cl,%al
62174f: js 621757 <stringlib_find_max_char.lto_priv.0+0x47>
621751: add $0x40,%rdi
621755: jmp 621712 <stringlib_find_max_char.lto_priv.0+0x2>
621757: mov $0xff,%eax
62175c: ret
62175d: nopl (%rax)
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Hmm, what build were the benchmarks run on? I'll test it.
Windows, MSVC. One thing here is that in the PGO run the path for size > 64 (my branch) or >SIZEOF_SIZE_T * 16 (this PR) is not exercised in find_max_char. Could you add a case to the tests in a PGO run and see whether that impacts the vectorization?
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Could you add a case to the tests in a PGO run and see whether that impacts the vectorization?
I can try, but I'd really rather not rely on such finicky things. We don't want to slow down platforms that don't vectorize for whatever reason, or strictly rely on the tests (distributors may use different sets for example).
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
|
I'll merge in a few days if there are no more comments.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
There are a few small cases where the approaches here are a bit slower. E.g. with b = b'\xe9' + b'a'*999 a call to b.isascii(). Minor compared to the gains for larger strings though.
I'm a bit concerned by the overhead of this change when first bytes/characters take this code path:
if (value & MASK_MAX_CHAR) {
return MAX_CHAR;
}
Currently, we load one size_t (8 bytes). With this change, 16 size_t are loaded (128 bytes). The change may make this case slower. My question is: how much slower is it?
'ucs1[8] slice[:-1]': (slice_tail, 'a' * 7 + LATIN1),
'ucs1[21] slice[:-1]': (slice_tail, 'a' * 20 + LATIN1),
'ucs1[1001] slice[:-1]': (slice_tail, 'a' * 1000 + LATIN1),
The benchmark seems to only measure cases when the fast-path is taken (when it's expected to be faster).
I would be interested by a benchmark on the worst case. For example, (slice_tail, LATIN1 + 'a' * 1000 + LATIN1).
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
That case is covered by the benchmark already, just under a different name. The ucs1[1001] non-ascii body slice[:-1] row is '\xe9' * 1000 + 'a', so the very first word has the mask bit set and the function returns at the first check, like it would for LATIN1 + 'a' * 1000 + LATIN1. The unread bytes after the first word don't matter for the timing. There are three more rows with a similar shape, bytes[1000] non-ascii decode latin-1 (b'\xe9' * 1000), ucs2[1001] wide body (first char > 0xff) and ucs4[1001] wide body (first char > 0xffff). The results for those rows, main vs. this PR:
|
PGO+LTO |
release build |
| ucs1[1001] non-ascii body slice[:-1] |
not significant |
1.02x slower |
| bytes[1000] non-ascii decode latin-1 |
1.02x faster |
1.02x faster |
| ucs2[1001] wide body slice[:-1] |
1.05x faster |
not significant |
| ucs4[1001] wide body slice[:-1] |
1.07x faster |
not significant |
So the extra reads are within noise in both directions. That matches what I would expect, the extra work is 15 word loads (8 SSE loads when vectorized) from data that is already in L1, which is a few nanoseconds against the ~45 ns for the slice operation. It is also why the block was reduced from the original 32 words to 16.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On Intel, memcpy(size_t) may be cheap on unaligned memory addresses, but on other architectures it might be more expensive. Would it be interesting to replace memcpy() with a size_t* pointer (and do the boring work to get an aligned pointer)?
For example, utf8_decode() checks _Py_IS_ALIGNED(s, ALIGNOF_SIZE_T) to get a const size_t * pointer and avoid memcpy().
Maybe switching to size_t* pointer can be investigated in a follow-up PR.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
On Intel, memcpy(size_t) may be cheap on unaligned memory addresses, but on other architectures it might be more expensive. Would it be interesting to replace memcpy() with a size_t* pointer (and do the boring work to get an aligned pointer)?
For example, utf8_decode() checks _Py_IS_ALIGNED(s, ALIGNOF_SIZE_T) to get a const size_t * pointer and avoid memcpy().
Maybe switching to size_t* pointer can be investigated in a follow-up PR.
The PR originally had exactly that, but I switched to memcpy() because it removed the prologue and on x86-64 it compiles to the same single load. But, testing now in Compiler Explorer, while that's true for x86-64 (GCC, Clang, MSVC), AArch64 and s390x, on RISC-V it becomes eight byte loads plus stores and a reload, and on SPARC64 it becomes a memcpy call per word, both of which would be slower than the current code. So I reverted that, the pointer is aligned first and the loops use size_t * loads again.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wrote a variant to address my suggestions: https://gist.github.com/vstinner/b47d2d3340607510a3d46b26955d83de
I wrote a stress test to check that find_max_char() works as expected:
import _testcapi
unicode_kind = _testcapi.unicode_kind
def kind(s):
if s.isascii():
return 0
else:
return unicode_kind(s)
for before in range(1, 10+1):
for after in range(1, 10+1):
for repeat in (1, 10, 100, 200, 1000):
for ch, next_ch, expected in (
('x', '\xe9', 1),
('\xe9', '\u20ac', 2),
('\u20ac', '\U0010ffff', 4),
):
# test different memory alignments
for start in range(16):
s = '#' * start + next_ch * before + ch * repeat + next_ch * after
assert kind(s[start + before:]) == expected
assert kind(s[start:-after]) == expected
assert kind(s[start + before:-after]) == kind(ch)
s = '#' * start + ch * repeat + next_ch + ch * repeat
assert kind(s[start:]) == expected
print("All good.")
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
|
Thanks Victor, I applied your suggestions. A little change from your gist is that the tail's if (n >= 4) is no longer inside #if SIZEOF_SIZE_T == 8, since the following else if would otherwise be a dangling else on 32-bit builds with 1- or 2-byte characters.
|
vstinner reacted with thumbs up emoji
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
vstinner
commented
Nov 7, 2026
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
|
I checked ucs1lib_find_max_char() assembly code on Linux x86-64: Fedora 45 with GCC 16.2.1. Python built with ./configure: gcc -O3.
The core of the while (p + SIZEOF_SIZE_T * 16 <= _end) { loop uses 16 or instructions on 64-bit:
0x0000000000783078 <+120>: mov rax,QWORD PTR [rdx-0x80]
0x000000000078307c <+124>: or rax,QWORD PTR [rdx-0x78]
0x0000000000783080 <+128>: or rax,rdi
0x0000000000783083 <+131>: or rax,QWORD PTR [rdx-0x70]
0x0000000000783087 <+135>: or rax,QWORD PTR [rdx-0x68]
0x000000000078308b <+139>: or rax,QWORD PTR [rdx-0x60]
0x000000000078308f <+143>: or rax,QWORD PTR [rdx-0x58]
0x0000000000783093 <+147>: or rax,QWORD PTR [rdx-0x50]
0x0000000000783097 <+151>: or rax,QWORD PTR [rdx-0x48]
0x000000000078309b <+155>: or rax,QWORD PTR [rdx-0x40]
0x000000000078309f <+159>: or rax,QWORD PTR [rdx-0x38]
0x00000000007830a3 <+163>: or rax,QWORD PTR [rdx-0x30]
0x00000000007830a7 <+167>: or rax,QWORD PTR [rdx-0x28]
0x00000000007830ab <+171>: or rax,QWORD PTR [rdx-0x20]
0x00000000007830af <+175>: or rax,QWORD PTR [rdx-0x18]
0x00000000007830b3 <+179>: or rax,QWORD PTR [rdx-0x10]
0x00000000007830b7 <+183>: or rax,QWORD PTR [rdx-0x8]
0x00000000007830bb <+187>: mov rdi,rax
0x00000000007830be <+190>: test rax,rcx
0x00000000007830c1 <+193>: jne 0x7831b8 <ucs1lib_find_max_char+440>
With a small change, the loop uses 8 por instructions on 128-bits + 8 movdqu (128-bit):
0x000000000078309e <+158>: movdqu xmm0,XMMWORD PTR [rdx-0x80]
0x00000000007830a3 <+163>: movdqu xmm2,XMMWORD PTR [rdx-0x70]
0x00000000007830a8 <+168>: movdqu xmm1,XMMWORD PTR [rdx-0x50]
0x00000000007830ad <+173>: movdqu xmm5,XMMWORD PTR [rdx-0x60]
0x00000000007830b2 <+178>: movdqu xmm3,XMMWORD PTR [rdx-0x40]
0x00000000007830b7 <+183>: movdqu xmm6,XMMWORD PTR [rdx-0x30]
0x00000000007830bc <+188>: por xmm0,xmm2
0x00000000007830c0 <+192>: movdqu xmm4,XMMWORD PTR [rdx-0x20]
0x00000000007830c5 <+197>: movdqu xmm7,XMMWORD PTR [rdx-0x10]
0x00000000007830ca <+202>: por xmm1,xmm5
0x00000000007830ce <+206>: por xmm0,xmm3
0x00000000007830d2 <+210>: por xmm1,xmm6
0x00000000007830d6 <+214>: por xmm1,xmm7
0x00000000007830da <+218>: por xmm0,xmm4
0x00000000007830de <+222>: por xmm0,xmm1
0x00000000007830e2 <+226>: movdqa xmm1,xmm0
0x00000000007830e6 <+230>: psrldq xmm1,0x8
0x00000000007830eb <+235>: por xmm0,xmm1
0x00000000007830ef <+239>: movq rax,xmm0
0x00000000007830f4 <+244>: test rax,r8
0x00000000007830f7 <+247>: je 0x783088 <ucs1lib_find_max_char+136>
The small change: use size_t local_value = 0; in the loop, rather than directly updating the "global" value variable.
diff --git a/Objects/stringlib/find_max_char.h b/Objects/stringlib/find_max_char.h
index 9f818b6abc7..8f8f750206e 100644
--- a/Objects/stringlib/find_max_char.h
+++ b/Objects/stringlib/find_max_char.h
@@ -84,12 +84,16 @@ STRINGLIB(find_max_char)(const STRINGLIB_CHAR *begin, const STRINGLIB_CHAR *end)
p += sizeof(uint32_t);
}
#endif
+ if (value & MASK_MAX_CHAR) {
+ return MAX_CHAR;
+ }
assert(_Py_IS_ALIGNED(p, ALIGNOF_SIZE_T));
}
while (p + SIZEOF_SIZE_T * 16 <= _end) {
/* Four independent accumulators so the loop does not serialize on
one register when the compiler does not vectorize it. */
+ size_t local_value = 0;
size_t v0 = 0, v1 = 0, v2 = 0, v3 = 0;
for (int i = 0; i < 16 * SIZEOF_SIZE_T; i += 4 * SIZEOF_SIZE_T) {
const size_t *w = (const size_t *)(p + i);
@@ -98,11 +102,12 @@ STRINGLIB(find_max_char)(const STRINGLIB_CHAR *begin, const STRINGLIB_CHAR *end)
v2 |= w[2];
v3 |= w[3];
}
- value |= v0 | v1 | v2 | v3;
- if (value & MASK_MAX_CHAR) {
+ local_value |= v0 | v1 | v2 | v3;
+ if (local_value & MASK_MAX_CHAR) {
return MAX_CHAR;
}
p += SIZEOF_SIZE_T * 16;
+ value |= local_value;
}
for (; p + SIZEOF_SIZE_T <= _end; p += SIZEOF_SIZE_T) {
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
I also checked clang, still with my size_t local_value = 0; change: also 8 movdqu (128-bit) with 8 por (128-bit), but a little bit different code to process the xmmN vectors
0x0000000000727efc <+172>: movdqu xmm0,XMMWORD PTR [rax+0x50]
0x0000000000727f01 <+177>: movdqu xmm1,XMMWORD PTR [rax+0x70]
0x0000000000727f06 <+182>: movdqu xmm2,XMMWORD PTR [rax+0x40]
0x0000000000727f0b <+187>: movdqu xmm3,XMMWORD PTR [rax+0x60]
0x0000000000727f10 <+192>: movdqu xmm4,XMMWORD PTR [rax]
0x0000000000727f14 <+196>: por xmm4,xmm2
0x0000000000727f18 <+200>: movdqu xmm2,XMMWORD PTR [rax+0x10]
0x0000000000727f1d <+205>: por xmm2,xmm0
0x0000000000727f21 <+209>: movdqu xmm0,XMMWORD PTR [rax+0x20]
0x0000000000727f26 <+214>: por xmm0,xmm3
0x0000000000727f2a <+218>: por xmm0,xmm4
0x0000000000727f2e <+222>: movdqu xmm3,XMMWORD PTR [rax+0x30]
0x0000000000727f33 <+227>: por xmm3,xmm1
0x0000000000727f37 <+231>: por xmm3,xmm2
0x0000000000727f3b <+235>: por xmm3,xmm0
0x0000000000727f3f <+239>: pshufd xmm0,xmm3,0xee
0x0000000000727f44 <+244>: por xmm0,xmm3
0x0000000000727f48 <+248>: movq r8,xmm0
0x0000000000727f4d <+253>: or rdx,r8
0x0000000000727f50 <+256>: mov rax,rdi
0x0000000000727f53 <+259>: test r8,rcx
0x0000000000727f56 <+262>: je 0x727ef0 <ucs1lib_find_max_char+160>
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
Aargh, I missed that it regressed. Thank you Victor! I applied your patch.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
I checked ucs1lib_find_max_char() in the latest version of the PR on Fedora 45 (GCC 16.2.1) with gcc -O3 (no PGO, no LTO).
The core of the function while (p + SIZEOF_SIZE_T * 16 <= _end) { uses 8 movdqu (128-bit) + 8 por (128-bit):
0x00000000007830de <+158>: movdqu xmm0,XMMWORD PTR [rdx-0x80]
0x00000000007830e3 <+163>: movdqu xmm2,XMMWORD PTR [rdx-0x70]
0x00000000007830e8 <+168>: movdqu xmm1,XMMWORD PTR [rdx-0x50]
0x00000000007830ed <+173>: movdqu xmm5,XMMWORD PTR [rdx-0x60]
0x00000000007830f2 <+178>: movdqu xmm3,XMMWORD PTR [rdx-0x40]
0x00000000007830f7 <+183>: movdqu xmm6,XMMWORD PTR [rdx-0x30]
0x00000000007830fc <+188>: por xmm0,xmm2
0x0000000000783100 <+192>: movdqu xmm4,XMMWORD PTR [rdx-0x20]
0x0000000000783105 <+197>: movdqu xmm7,XMMWORD PTR [rdx-0x10]
0x000000000078310a <+202>: por xmm1,xmm5
0x000000000078310e <+206>: por xmm0,xmm3
0x0000000000783112 <+210>: por xmm1,xmm6
0x0000000000783116 <+214>: por xmm1,xmm7
0x000000000078311a <+218>: por xmm0,xmm4
0x000000000078311e <+222>: por xmm0,xmm1
0x0000000000783122 <+226>: movdqa xmm1,xmm0
0x0000000000783126 <+230>: psrldq xmm1,0x8
0x000000000078312b <+235>: por xmm0,xmm1
0x000000000078312f <+239>: movq rax,xmm0
0x0000000000783134 <+244>: test rax,r8
0x0000000000783137 <+247>: je 0x7830c8 <ucs1lib_find_max_char+136>
The surprising part for me is that GCC decided to also unroll (6 iterations) and vectorize the smaller loop for (; p + SIZEOF_SIZE_T <= _end; p += SIZEOF_SIZE_T) {: 6 movdqu (128-bit) and 6 por (128-bit).
0x000000000078e3b1 <+657>: movdqu xmm2,XMMWORD PTR [rdx+0x10]
0x000000000078e3b6 <+662>: por xmm0,xmm2
0x000000000078e3ba <+666>: cmp r10,0x2
0x000000000078e3be <+670>: je 0x78e405 <_PyUnicode_FromUCS1+741>
0x000000000078e3c0 <+672>: movdqu xmm3,XMMWORD PTR [rdx+0x20]
0x000000000078e3c5 <+677>: por xmm0,xmm3
0x000000000078e3c9 <+681>: cmp r10,0x3
0x000000000078e3cd <+685>: je 0x78e405 <_PyUnicode_FromUCS1+741>
0x000000000078e3cf <+687>: movdqu xmm4,XMMWORD PTR [rdx+0x30]
0x000000000078e3d4 <+692>: por xmm0,xmm4
0x000000000078e3d8 <+696>: cmp r10,0x4
0x000000000078e3dc <+700>: je 0x78e405 <_PyUnicode_FromUCS1+741>
0x000000000078e3de <+702>: movdqu xmm5,XMMWORD PTR [rdx+0x40]
0x000000000078e3e3 <+707>: por xmm0,xmm5
0x000000000078e3e7 <+711>: cmp r10,0x5
0x000000000078e3eb <+715>: je 0x78e405 <_PyUnicode_FromUCS1+741>
0x000000000078e3ed <+717>: movdqu xmm6,XMMWORD PTR [rdx+0x50]
0x000000000078e3f2 <+722>: por xmm0,xmm6
0x000000000078e3f6 <+726>: cmp r10,0x6
0x000000000078e3fa <+730>: je 0x78e405 <_PyUnicode_FromUCS1+741>
0x000000000078e3fc <+732>: movdqu xmm7,XMMWORD PTR [rdx+0x60]
0x000000000078e401 <+737>: por xmm0,xmm7
0x000000000078e405 <+741>: movdqa xmm1,xmm0
0x000000000078e409 <+745>: psrldq xmm1,0x8
0x000000000078e40e <+750>: por xmm0,xmm1
0x000000000078e412 <+754>: movq r8,xmm0
0x000000000078e417 <+759>: or rcx,r8
0x000000000078e41a <+762>: lea r8,[r10+r10*1]
0x000000000078e41e <+766>: cmp r8,rbx
0x000000000078e421 <+769>: je 0x78e42d <_PyUnicode_FromUCS1+781>
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. But I just have one last coding style suggestion :-)
I checked the PR on my stress test and it passed!
Using size_t* should help the compiler to vectorize the code and use the most efficient available instructions for that task. At least, I could check that on x86-64, SIMD is used.
I didn't run a benchmark, but I expected that SMID working on 128-bit vectors should be more efficient than the current code!
The current code is more pessimistic: it checks that each size_t word (8 bytes) exceeds the limit. This PR is more optimistic and only check every 16 size_t words (128 bytes) that the limite is exceeded. The optimistic approach is more efficient and it seems like only have a "minor" impact on the unlucky code path (when we hit the limit). It just reads too many bytes, but it's not a big deal. CPUs like to read multiple bytes forward, they should be well optimized for that (CPU cache lines).
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
|
@StanFromIreland: I understand that the benchmark numbers were on the memcpy() PR. You rewrote the PR to use size_t* pointers instead. Would you mind to re-run benchmarks?
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
Sure, here are benchmarks at the current commit:
So, after some testing I pushed a little change to use four accumulators and a 16-word block so the loop is faster even when it's not vectorised. Benchmarking again, some more promising results:
AMD Ryzen 7 8845HS, --enable-optimizations & --with-lto, GCC 15.3.1, Linux-7.1.13-100
| Benchmark |
maxchar-main |
maxchar-pr-final |
| control: ascii[1001] slice[:-1] |
85.1 ns |
84.3 ns: 1.01x faster |
| ucs1[8] slice[:-1] |
45.3 ns |
43.6 ns: 1.04x faster |
| ucs1[21] slice[:-1] |
48.0 ns |
43.9 ns: 1.09x faster |
| ucs1[1001] slice[:-1] |
120 ns |
118 ns: 1.02x faster |
| ucs1[1001] slice[1:] (odd ptr) |
124 ns |
122 ns: 1.01x faster |
| ucs1[1001] slice[3:] |
124 ns |
123 ns: 1.01x faster |
| ucs1[1001] non-ascii body slice[:-1] |
73.6 ns |
74.0 ns: 1.01x slower |
| ucs1[100001] slice[:-1] |
6.71 us |
6.05 us: 1.11x faster |
| ucs1[10_000_001] slice[:-1] |
788 us |
660 us: 1.19x faster |
| ucs2[21] slice[:-1] |
48.4 ns |
45.0 ns: 1.08x faster |
| ucs2[1002] slice[:-1] (issue) |
195 ns |
111 ns: 1.77x faster |
| ucs2[1001] latin1 body slice[:-1] |
195 ns |
113 ns: 1.73x faster |
| ucs2[1001] slice[1:] |
195 ns |
117 ns: 1.66x faster |
| ucs2[1001] wide body slice[:-1] |
91.5 ns |
88.0 ns: 1.04x faster |
| ucs4[21] slice[:-1] |
50.7 ns |
49.1 ns: 1.03x faster |
| ucs4[1001] slice[:-1] |
309 ns |
247 ns: 1.25x faster |
| ucs4[1001] wide body slice[:-1] |
101 ns |
97.9 ns: 1.04x faster |
| bytes[10] isascii |
36.4 ns |
36.2 ns: 1.00x faster |
| bytes[1000] isascii |
61.5 ns |
50.1 ns: 1.23x faster |
| bytes[1000] decode latin-1 |
134 ns |
132 ns: 1.01x faster |
| bytes[1000] non-ascii decode latin-1 |
82.7 ns |
84.5 ns: 1.02x slower |
| ucs1 200 lines x 51 split |
5.69 us |
5.82 us: 1.02x slower |
| ucs1 200 lines x 11 split |
3.67 us |
3.45 us: 1.07x faster |
| ucs2 200 lines x 51 split |
8.57 us |
7.59 us: 1.13x faster |
| Geometric mean |
(ref) |
1.13x faster |
Intel(R) Core(TM) i5-7500T CPU @ 2.70GHz, release-build, GCC 15.2.1, Linux-6.14.0-63
| Benchmark |
maxchar-main |
maxchar-pr-8nov |
| control: ascii[1001] slice[:-1] |
228 ns |
218 ns: 1.05x faster |
| ucs1[8] slice[:-1] |
143 ns |
138 ns: 1.04x faster |
| ucs1[1001] slice[:-1] |
286 ns |
245 ns: 1.17x faster |
| ucs1[1001] slice[1:] (odd ptr) |
295 ns |
252 ns: 1.17x faster |
| ucs1[1001] slice[3:] |
293 ns |
250 ns: 1.17x faster |
| ucs1[1001] non-ascii body slice[:-1] |
227 ns |
229 ns: 1.01x slower |
| ucs1[100001] slice[:-1] |
7.60 us |
4.95 us: 1.54x faster |
| ucs1[10_000_001] slice[:-1] |
1.24 ms |
991 us: 1.25x faster |
| ucs2[21] slice[:-1] |
145 ns |
146 ns: 1.01x slower |
| ucs2[1002] slice[:-1] (issue) |
483 ns |
278 ns: 1.74x faster |
| ucs2[1001] latin1 body slice[:-1] |
521 ns |
317 ns: 1.65x faster |
| ucs2[1001] slice[1:] |
526 ns |
320 ns: 1.64x faster |
| ucs4[21] slice[:-1] |
148 ns |
148 ns: 1.00x slower |
| ucs4[1001] slice[:-1] |
808 ns |
482 ns: 1.68x faster |
| ucs4[1001] wide body slice[:-1] |
257 ns |
254 ns: 1.01x faster |
| bytes[10] isascii |
92.9 ns |
96.4 ns: 1.04x slower |
| bytes[1000] isascii |
151 ns |
119 ns: 1.26x faster |
| bytes[1000] decode latin-1 |
308 ns |
264 ns: 1.17x faster |
| bytes[1000] non-ascii decode latin-1 |
256 ns |
249 ns: 1.03x faster |
| ucs1 200 lines x 51 split |
15.0 us |
11.8 us: 1.28x faster |
| ucs1 200 lines x 11 split |
9.74 us |
8.11 us: 1.20x faster |
| ucs2 200 lines x 51 split |
17.2 us |
12.2 us: 1.41x faster |
| Geometric mean |
(ref) |
1.20x faster |
Benchmark hidden because not significant (2): ucs1[21] slice[:-1], ucs2[1001] wide body slice[:-1]
PGO+LTO is a little slower than before, but that seems to be because of PGO poorly optimising _PyUnicode_FromUCS1 based on some debugging.
If there are no more comments, I will merge this tomorrow. :-)
|
vstinner reacted with thumbs up emoji
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
|
Yeah! That's a cool change, I like it!
|
StanFromIreland reacted with heart emoji
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Reviewers
vstinner
vstinner approved these changes
picnixz
picnixz approved these changes
Uh oh!
There was an error while loading. Please reload this page.
Milestone
No milestone
Development
Successfully merging this pull request may close these issues.
Uh oh!
There was an error while loading. Please reload this page.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
|