Skip to content

perf(sax): intern attribute names in the engine hot path#16

Merged
evoactivity merged 1 commit into
mainfrom
perf/sax-string-allocations
Jul 19, 2026
Merged

perf(sax): intern attribute names in the engine hot path#16
evoactivity merged 1 commit into
mainfrom
perf/sax-string-allocations

Conversation

@evoactivity

Copy link
Copy Markdown
Owner

What

Interns attribute names in the SAX engine hot path via a small direct-mapped cache (512 slots, cheap length/first/last-char fingerprint, verify loop for collisions).

Why

Attribute names repeat constantly, and every fresh attribute-name substring handed to the keyed attribute store must be re-internalized by V8 before it can be used as a property key. Returning the same string instance for repeated names means repeat occurrences allocate nothing and the store receives an already-internalized key.

Measured results (Node 22, isolated processes, medians)

workload before after
@tuananh/sax-parser repo benchmark (attr-heavy synthetic, single write) 25,234 op/s 27,206 op/s (+7.8%)
tokenize: attr-heavy synthetic (256 B chunks) 15,605 op/s 17,834 op/s (+14.3%)
tokenize: XMLTV EPG (256 B chunks) 20,970 op/s 22,401 op/s (+6.8%)
tokenize: RSS / Maven POM neutral

Scope is deliberate

Interning is applied to attribute names only. Two rejected variants, kept out with a code comment so they don't get re-attempted:

  • Interning tag names as well: large net loss on element-dense documents (RSS -22%, POM -24%) — tag names are handler arguments, never property keys, so they pay the cache cost for no internalization benefit.
  • Full-content hash instead of the cheap fingerprint: -38% overall; the hash loop cost more than the substring allocation it avoided.

Also experimented with (and rejected) a manual short-scan fast path replacing indexOf for attribute values: it only wins on sub-8-char values (the synthetic benchmark shape) and loses ~5-7% on EPG's ~20-char timestamp values — V8's vectorized indexOf is too good at realistic value lengths.

All 838 tests pass.

Repeated attribute names previously materialized a fresh substring per
occurrence, and every fresh string handed to the keyed attribute store
must be re-internalized by V8 before use as a property key. A small
direct-mapped cache (cheap length/first/last fingerprint plus verify
loop) returns the same instance for repeated names, so repeat
occurrences allocate nothing and the store receives an internalized key.

Scoped to attribute names only: benchmarks showed interning tag names
is a large net loss on element-dense documents (RSS -22%, POM -24%),
while attribute-name interning gains 5-14% on attribute-heavy
documents and is neutral elsewhere.
@evoactivity evoactivity added enhancement New feature or request bug Something isn't working and removed enhancement New feature or request labels Jul 19, 2026
@evoactivity
evoactivity merged commit 2acdd58 into main Jul 19, 2026
8 of 10 checks passed
@github-actions github-actions Bot mentioned this pull request Jul 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant