Skip to content

Commit 60eca2d

Browse files
cmaloneyclaudevstinner
authored
gh-161145: Test more bytes simultaneously in find_first_nonascii (#161147)
Long ASCII runs now OR eight aligned words and test the high bits once, one branch per 64 bytes (32 on 32-bit) instead of one per word. When a block contains a non-ASCII byte, the existing one-word loop finds it. It is plain C: GCC and Clang compile it to SSE2/NEON loads, other compilers to ORed word loads with the same single branch. Multiple compiler and architecture codegen sample: https://godbolt.org/z/8bM83vx3a pyperf, PGO+LTO, 2 interleaved rounds: | Benchmark | main | this | |---|---:|---:| | (b'a' * 4096).decode() | 165 ns | 108 ns: 1.53x faster | | (b'a' * 200).decode() | 28.9 ns | 26.5 ns: 1.09x faster | | b'a' * 200 + 'é' + ..., .decode() | 60.9 ns | 56.0 ns: 1.09x faster | | b'a' * 136 + 'é' + ..., .decode() | 53.8 ns | 51.3 ns: 1.05x faster | | 8-100 byte decodes, 'é' within 100 | | within 1.2 ns | ```python import pyperf runner = pyperf.Runner() tail = 'é'.encode() + b'a' * 50 for n in (8, 100, 200, 4096): runner.timeit(f"(b'a' * {n}).decode()", "s.decode()", globals={"s": b'a' * n}) for n in (16, 100, 136, 200): runner.timeit(f"(b'a' * {n} + 'é'.encode() + b'a' * 50).decode()", "s.decode()", globals={"s": b'a' * n + tail}) ``` Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Victor Stinner <vstinner@python.org>
1 parent edd6f76 commit 60eca2d

2 files changed

Lines changed: 12 additions & 0 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Speed up UTF-8 decoding of long ASCII runs, such as in :meth:`bytes.decode`,
2+
by up to 1.5x.

‎Objects/unicodeobject.c‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5063,6 +5063,16 @@ find_first_nonascii(const unsigned char *start, const unsigned char *end)
50635063
}
50645064
#endif
50655065

5066+
// Long ASCII runs: test eight words per branch.
5067+
for (; end - p >= 8 * SIZEOF_SIZE_T; p += 8 * SIZEOF_SIZE_T) {
5068+
const size_t *w = (const size_t *)p;
5069+
if ((w[0] | w[1] | w[2] | w[3] |
5070+
w[4] | w[5] | w[6] | w[7]) & ASCII_CHAR_MASK)
5071+
{
5072+
break;
5073+
}
5074+
}
5075+
50665076
const unsigned char *e = end - SIZEOF_SIZE_T;
50675077
while (p <= e) {
50685078
size_t u = (*(const size_t *)p) & ASCII_CHAR_MASK;

0 commit comments

Comments
 (0)