Skip to content

gh-122901: Speed up str creation from non-ASCII data with a branch-free find_max_char() - #122902

Open
ruema wants to merge 8 commits into
python:mainfrom
ruema:feature/speedup_find_max_char
Open

ruema wants to merge 8 commits into
python:mainfrom
ruema:feature/speedup_find_max_char

Conversation

@ruema

@ruema ruema commented Aug 11, 2024 •

Copy link
Copy Markdown

Change to find_max_char to speedup string creation.
See issue #122901.

@github-actions

This comment was marked as resolved.

@github-actions github-actions Bot added the stale Stale PR or inactive for long period of time. label Apr 18, 2026
@bedevere-app bedevere-app Bot added the type-feature A feature request or enhancement label Oct 30, 2026
@StanFromIreland StanFromIreland self-assigned this Oct 30, 2026
@StanFromIreland 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
@StanFromIreland StanFromIreland added the 🔨 test-with-buildbots Test PR w/ buildbots; report in status section label Oct 30, 2026
@bedevere-bot

This comment was marked as resolved.

@bedevere-bot bedevere-bot removed the 🔨 test-with-buildbots Test PR w/ buildbots; report in status section label Oct 30, 2026
@StanFromIreland

Copy link
Copy Markdown
Member

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...

@StanFromIreland

Copy link
Copy Markdown
Member

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.

@StanFromIreland StanFromIreland added the 🔨 test-with-buildbots Test PR w/ buildbots; report in status section label Oct 30, 2026
@bedevere-bot

Copy link
Copy Markdown

🤖 New build scheduled with the buildbot fleet by @StanFromIreland for commit 1daa7ee 🤖

Results will be shown at:

https://buildbot.python.org/all/#/grid?branch=refs%2Fpull%2F122902%2Fmerge

If you want to schedule another build, you need to add the 🔨 test-with-buildbots label again.

@bedevere-bot bedevere-bot removed the 🔨 test-with-buildbots Test PR w/ buildbots; report in status section label Oct 30, 2026
@StanFromIreland

Copy link
Copy Markdown
Member

I think I'm pretty happy with this, @vstinner, @eendebakpt and @picnixz what do you think?

@eendebakpt

eendebakpt commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

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.

@StanFromIreland

StanFromIreland commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

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?

@StanFromIreland

StanFromIreland commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

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)

@eendebakpt

Copy link
Copy Markdown
Contributor

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?

@StanFromIreland

Copy link
Copy Markdown
Member

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).

@StanFromIreland

Copy link
Copy Markdown
Member

I added a little test, and it didn't work, it likely just wasn't enough. We could try more test cases to emphasise the path, but at that point we're still playing a PGO lottery, and that brings me back to my previous points on this. As such, what do you think about the current approach, which gives a reasonable improvement (and generally no slowdowns, compared to the other patches) even when unvectorized?

@chris-eibl

Copy link
Copy Markdown
Member

Windows, MSVC. One thing here is that in the PGO

Did you use --tail-call-interp? This can have a positive effect in PGO decisions (and hence is what we ship since 3.15).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting review stale Stale PR or inactive for long period of time. type-feature A feature request or enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants