perf: use plain records to avoid V8 IC churn from null prototypes#12
Merged
Conversation
Null-prototype objects (Object.create(null) and { __proto__: null }
literals) get a unique dictionary-mode map per object in V8, making
the record store site's inline cache churn on every element. On the
large parse functions this permanently blocks TurboFan ("delaying
optimization, IC changed"), costing up to 7x throughput for elements
with exactly one attribute.
Attribute and lossy element records are now plain objects, which share
hidden classes so store sites stay monomorphic. Prototype pollution
stays impossible: the new setOwnProperty helper stores __proto__ via
defineProperty so dangerous names become own properties and
Object.prototype is never touched. Lossy sibling grouping now uses
Object.hasOwn instead of `in`, which on plain objects would have
matched inherited keys like `constructor`.
1-attr cliff, before -> after (ops/s, 10KB doc):
sax 3.1k -> 20.4k, parse 3.5k -> 14.1k,
stream 2.6k -> 9.6k, lossy 1.9k -> 9.0k
Byte-identical to the ~10 KB document generated by @tuananh/sax-parser's benchmark: 158 tiny elements with one attribute each. This attribute-dense shape exposed the null-prototype IC churn cliff that the existing attribute-light real-world fixtures never triggered. Wired into the tokenize, parse, and stream comparison suites as a regression guard.
Rerun of the full suite on Node 24 after the plain-record perf fix. Adds the attr-heavy synthetic column to the parsing, streaming, and tokenization tables. Serialization table reordered: with the fix, eksml (validate: false) now leads every fixture, so tXml no longer holds the small/SOAP/Atom/EPG leads.
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Elements with exactly one attribute parsed up to 7x slower than elements with zero or several attributes:
Profiling showed
processChunknever reaches TurboFan on 1-attr workloads.--trace-opt-verbosereports[delaying optimization of processChunk, IC changed]indefinitely: null-prototype objects (Object.create(null)/{ __proto__: null }literals) get a unique dictionary-mode map per object in V8, so the attribute store site's inline cache churns on every tag and V8 permanently postpones optimizing the parse functions. With 2+ attributes the IC goes megamorphic (stable), which is why only the single-attribute case fell off the cliff.Fix
Attribute and lossy element records are now plain
{}objects, which share hidden classes so store sites stay monomorphic.Prototype pollution remains impossible:
setOwnPropertyhelper stores__proto__viaObject.defineProperty, so dangerous names become own properties andObject.prototypeis never touched.Object.hasOwninstead ofin. On plain objects,inmatches inherited keys, so a<constructor>child would previously have taken the wrong grouping branch.Results (Node 22.22, 10KB doc, isolated processes)
1-attr cliff, before vs after:
createSaxParser)parse)XmlParseStream)All other attribute counts improve or are unchanged (within noise); the 0-4 attr curve is now monotonic. 838/838 tests pass.