Repository navigation
perf(codegen): Use char codes for simple classes - #660
Conversation
|
let me know if you need the benchmark or something else |
|
Please rebase, add yourself to AUTHORS, and add to the CHANGELOG. |
|
Also, this could use a few more tests, please. Include more complicated combinations that both should and should not use the new path. |
Simple character classes were emitted as regexp tests for every accepted character. Hot parser loops like keys and identifiers pay for that over and over. Emit direct charCodeAt comparisons for simple non-unicode, case-sensitive classes and reuse a temp char code inside each check. Keeps the regexp path for the complex cases. Co-Authored-By: Codex GPT-5 <noreply@openai.com>
Adds codegen tests for classes that should use charCodeAt (mixed ranges,
inverted, escaped `]\^-`, control chars, surrogate ranges) and ones that
should stay on RegExp (`i`, `u`, `\p{}`, astral). Plus behavior tests
for the same shapes, EOF on inverted classes, and a grammar mixing both
paths.
Also adds the changelog entry, adds me to AUTHORS, and regenerates the
cli fixtures for the new `peg$charCode` declaration.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Big classes like the unicode categories in the javascript example were turning into one giant chain of comparisons, 2-3x slower than the regexp on chars that miss. Classes with more than 32 parts now stay on the regexp. The crossover was somewhere between 32 and 64 ranges. Classes that use charCodeAt no longer emit their unused `peg$rN` constant either. Generated size for the javascript example goes from +10% to +0.4% vs main, xml from +20% to +2.5%. Also fixes two type errors on class escapes and drops the empty class fallback, empty classes never match so they never reach codegen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
reran the sentry search grammar benchmark against current main, it's 21-27% faster now instead of 14-19% Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
89e497b to
df5eeed
Compare
|
Rebased, added myself to AUTHORS, and added a CHANGELOG entry. Added tests for classes that should take the new path (mixed ranges, inverted, escaped Also, big classes like the unicode categories in the javascript example were 2-3x slower as a comparison chain, so classes over 32 parts now stay on the regexp. Unused |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #660 +/- ##
=======================================
Coverage 99.49% 99.49%
=======================================
Files 34 34
Lines 4314 4314
=======================================
Hits 4292 4292
Misses 22 22 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
I'm going to finish looking at this in the morning. I had a quick idea though: is there some number of codepoints at which throwing them into a |
|
LGTM. Thanks! |
|
Released in v5.1.1 |
port from peggy peggyjs/peggy#660
This optimizes generated parser code for simple character classes.
A lot of grammars have hot loops that look like this:
Peggy currently emits a regexp test for each character consumed by those classes. This PR emits direct
charCodeAtcomparisons when the class is simple enough to do that exactly: noiflag, no unicode mode, only single-code-unit ranges/characters, and at most 32 parts. Classes that take the new path also skip emitting their now-unusedpeg$rNregexp.Anything more complicated still uses the existing regexp path. The 32 part cap is there because huge classes, like the unicode category classes in
examples/javascript.pegjs, were 2-3x slower as one long chain of comparisons than as a regexp. The crossover was somewhere between 32 and 64 ranges.As one real-world benchmark, I used Sentry’s search grammar, which is the grammar we use for parsing search bar queries: https://github.com/getsentry/sentry/blob/master/static/app/components/searchSyntax/grammar.pegjs. That grammar has a lot of key/value filters, so longer searches spend plenty of time in these character-class loops.
AND, about 5.4 KBOR, about 5.4 KBBefore is current
main, Node 26.11.1, median of 3 runs where each run is the median of 500 parses.hyperfine agrees end to end, node startup included: 400 rounds of the short strings went from 1.306 s to 1.047 s (-19.8%), and 300 rounds of the long queries went from 1.309 s to 1.046 s (-20.1%).
The generated parser for that grammar is about 3% bigger (123 KB → 127 KB). For the example grammars it's +0.4% (javascript) to +3% (css).