Skip to content

Latest commit

 

History

History
70 lines (54 loc) · 3.54 KB

File metadata and controls

70 lines (54 loc) · 3.54 KB

Making CPython's string search 4–8× faster with SIMD

One-sentence result

CPython's multi-byte substring search — behind str.find, in, .count, .replace, .split, the re literal prefilter, and every bytes/bytearray equivalent — has no SIMD; a ~60-line NEON/SSE2 patch makes it 4–8× faster on large inputs, verified correct against 220,000+ differential fuzz cases and CPython's own test suite.

Sober headline

Python's str.find/in/.count leave 4–8× on the table for multi-byte needles on large strings — here's a patch that fixes it, with the worst-case guarantee kept.

Why it matters

Substring search is one of the most-executed primitives in the language: log parsing, template rendering, tokenizers, if x in line, re scans with a literal prefix. CPython already vectorises single-byte search (via memchr), which runs an order of magnitude faster than the multi-byte path — the whole gap is that nobody vectorised the ≥2-byte case. On an Apple M1 Pro, bytes.count for realistic needles runs at 1.5–3.8 GB/s today and 10–17 GB/s with the patch.

What it is — and isn't

The vector technique (first-byte + last-byte filter) is standard — glibc, ripgrep and StringZilla all use it. The contribution here is bringing it into CPython's own str/bytes, wired so that CPython's O(n) worst-case guarantee survives (an adaptive fall-back to the existing two-way algorithm on adversarial needles), and doing it with a correctness harness strong enough to trust: a quarter-million differential fuzz cases against a naive reference, plus the CPython string/regex/stdlib test suites.

It is honest about scope: the win is on large-input search. Short-string operations are dominated by interpreter/allocation overhead and change little. Only NEON was benchmarked locally; the SSE2 path is written but needs an x86 run.

HN-style post (no overclaim)

Title: CPython's multi-byte string search has no SIMD — a 60-line patch makes it 4–8× faster

Body: str.find/in/.count/.replace and their bytes cousins fall back to a scalar Bloom/Horspool loop for any needle longer than one byte, so on an M1 they run at 1.5–3.8 GB/s while single-byte search (memchr) does 40+. The patch adds a NEON/SSE2 first+last-byte filter to Objects/stringlib/fastsearch.h and keeps CPython's linear worst-case bound via the existing two-way algorithm as a fallback. Measured 4.4–7.5× on bytes.count over 60 MB of mixed text/code; str (ASCII/Latin-1) is the same. 220k differential-fuzz cases pass and so do test_bytes/test_str/test_re. Caveats: the win is scan-bound (large inputs), the technique is well known (glibc/ripgrep/StringZilla) — the novelty is getting it into CPython with the worst-case guarantee intact — and I've only benchmarked ARM. Patch + benchmark + fuzzer in the repo.

Attribution

The first+last SIMD filter is due to the wider string-search community (Wojciech Muła's writeups; glibc memmem; Andrew Gallant's memchr crate; Ash Vardanian's StringZilla). CPython's existing two-way implementation (used here as the worst-case fallback) is Dennis Sweeney's. This work only adds the CPython integration, the correctness/benchmark harness, and the honest measurement.

Recommended sequence (pending operator approval)

  1. Post the patch + benchmark to the CPython issue tracker / discuss.python.org as a proposal, explicitly asking for an x86 benchmark.
  2. If a maintainer confirms x86 numbers, open a PR referencing the fuzz harness.
  3. Only then, a write-up. No claim of "merged" unless it is.