Skip to content

perf: ASCII fast path for find_str_nocase, verified against the collator - #723

Merged
ddehilster merged 1 commit into
masterfrom
perf/ascii-fast-path-match
Aug 25, 2026
Merged

perf: ASCII fast path for find_str_nocase, verified against the collator#723
ddehilster merged 1 commit into
masterfrom
perf/ascii-fast-path-match

Conversation

@ddehilster

Copy link
Copy Markdown
Member

Where the time is now

With the I/O and stack-frame work done in 3.8.7/3.8.9, a fresh profile of released 3.8.9 shows the compiled runtime is dominated by case-insensitive string matching:

run!find_str_nocase 32.3% inclusive
run!Arun::match_list 28.6%
icu::Collator::compare 13.2%
icu::UnicodeString::fromUTF8 10.3%
icu::RuleBasedCollator::doCompare 9.6%

For comparison, ntdll!ZwWriteFile — 25.5% back in 3.8.4 — is now 2.2%, and _chkstk is gone. A third of the run is matching a node's name against a rule element's match/fail/except list through an ICU collator at PRIMARY strength.

The change

When both sides are printable ASCII, PRIMARY-strength equality is exactly byte-wise case-insensitive equality, so ICU can be skipped.

That claim is verified, not assumed. A test program built against the same ICU compared the collator against a byte-wise compare:

corpuspairsmismatches
all printable-ASCII character pairs (0x20–0x7E)9,0250
word / punctuation / spacing variants1,5210
control characters (0x01–0x1F)12478

That last row is why the guard is "printable ASCII" and not "ASCII". Control characters are primary-ignorable — the collator considers "a\x01" equal to "a". Bytes ≥ 0x80 are excluded too, since PRIMARY strength also folds accents (so "cafe" matches an accented "café", which may well be deliberate in an NLP engine). Either case sends that one comparison to ICU, so no matching behaviour changes.

The needle's UTF-16 form is also built lazily now, so a lookup whose candidates are all plain ASCII — overwhelmingly the common case — does no transcoding at all.

Results

Interleaved A/B, 8 runs per arm, swapping both nlp.exe and run.dll:

minp25median
ICU collator (3.8.9)7.78s8.10s8.75s
ASCII fast path7.02s7.49s8.09s

1.08x – 1.11x. Smaller than the 32% inclusive figure might suggest, because much of that is match_list's own work rather than ICU. The mechanism did engage: RuleBasedCollator::doCompare, ::compare and CollationFastLatin::compareUTF16 all drop out of the profile entirely, and u_strFromUTF8WithSub halves.

Correctness

All 18 analyzer output files byte-identical. That's the strongest check available here — this function decides what the matcher matches, so any semantic drift would show up as different parse output.

🤖 Generated with Claude Code

After 3.8.9 the compiled runtime is dominated by case-insensitive string
matching. Profiling parse-en-us:
find_str_nocase 32.3% inclusive
Arun::match_list 28.6%
icu::Collator::compare 13.2%
icu::UnicodeString::fromUTF8 10.3%
icu::RuleBasedCollator::doCompare 9.6%
A third of the run is matching a node's name against a rule element's
match/fail/except list, through an ICU collator at PRIMARY strength.
When both sides are printable ASCII, PRIMARY-strength equality is exactly
byte-wise case-insensitive equality, so ICU can be skipped. That claim is
verified rather than assumed. A test program built against the same ICU
compared the collator with a byte-wise compare over:
all printable-ASCII character pairs (0x20-0x7E) 9,025 pairs, 0 mismatches
word/punctuation/spacing variants 1,521 pairs, 0 mismatches
control characters (0x01-0x1F) 124 pairs, 78 MISMATCHES
Control characters are primary-IGNORABLE: the collator considers "a\x01"
equal to "a". They are therefore excluded from the fast path, as are bytes
>= 0x80, which need real collation because PRIMARY strength also folds
accents ("cafe" matches an accented "cafe"). Either sends that one
comparison to ICU, so no matching behaviour changes.
The needle's UTF-16 form is now built lazily too, so a lookup whose
candidates are all plain ASCII -- overwhelmingly the common case -- does no
transcoding at all.
Interleaved A/B, 8 runs per arm, swapping nlp.exe and run.dll:
ICU collator (3.8.9) min 7.78s p25 8.10s median 8.75s
ASCII fast path min 7.02s p25 7.49s median 8.09s 1.08x - 1.11x
RuleBasedCollator::doCompare, ::compare and CollationFastLatin::compareUTF16
all drop out of the profile entirely; u_strFromUTF8WithSub halves.
All 18 analyzer output files byte-identical -- the strongest check
available here, since this function decides what the matcher matches.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ddehilster
ddehilster merged commit 52f7852 into masterAug 25, 2026
7 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@ddehilster