Repository navigation
Order the indexed inline cache by observed receiver kind: a lazy array pays ~49% of its read in guards that cannot match #10118
Description
The problem
Every indexed read on an unknown receiver walks a fixed chain of guards in emitted order, and a receiver only reaches the tier that can serve it after every earlier tier has failed. For a JSON.parse array — the common case in any JSON-shaped workload — that means paying for two whole families of guards that can never match.
Static breakdown of the rows[7].id loop in the JSON access benchmark (688 IR instructions, from --trace llvm on the post-#10114 compiler):
| block family | IR instrs | share |
|---|---|---|
| array-index IC tiers (ordinary / subclass / shape / family / spill) | 212 | 30.8% |
property IC (.id) |
183 | 26.6% |
typed-array probe (tav.*) |
123 | 17.9% |
| loop / arithmetic | 104 | 15.1% |
| string equality | 30 | 4.4% |
| GC poll + root barrier | 29 | 4.2% |
| lazy probe (the tier that actually serves the read) | 7 | 1.0% |
The tier that serves the read is 1% of the footprint. The typed-array probe and the array-index tiers — ~49% — exist to be missed by this receiver, every iteration, forever.
Measured cost after #10114: records_array_16k:repeat is 116.7 retired instructions per rows[7].id, of which ~25 are inside the probe and ~92 are the emitted guard chain. Node does the whole read in roughly 8 cycles. The remaining access rows sit at 1.47×–4.58× the better of Node/Bun.
Proposal: order the chain by observed receiver kind
The IC site already carries a per-site cache slot (#9708 made them lazily allocated, 8 bytes, primed on first use). It records a shape identity today; it does not record what kind of receiver was seen.
Record the observed GcHeader::obj_type on prime, and branch on it first:
GC_TYPE_LAZY_ARRAY→ straight tojs_lazy_array_index_probeGC_TYPE_ARRAY→ straight toarrlike.ic.array_guard- typed array → straight to the
tav.*tier - anything else, or a kind change → the existing full chain, unchanged
This is not a new proof — every guard that currently runs still runs on the path that needs it. It only stops a receiver from paying for the tiers that provably cannot match it. A polymorphic site that sees several kinds degrades to today's behaviour.
Why this is the right next lever
- It is the dominant remaining cost on the worst rows in the JSON matrix, and those rows are the last family still behind Node and Bun.
- It is receiver-kind-generic: typed arrays and ordinary Arrays behind an erased receiver benefit for the same reason, not just JSON.
- It composes with, rather than duplicates, Unify lazy JSON arrays into GC_TYPE_ARRAY: laziness as element state, not an object type #10098 — that issue removes
GC_TYPE_LAZY_ARRAYas a distinct kind entirely, at which point this dispatch has one fewer arm but the same shape.
Validation it needs
- The emitted chain must be provably unchanged for a receiver whose recorded kind is absent or stale — an IR claim test per tier.
- Kind-transition coverage: a site that sees a lazy array, then an ordinary Array, then a typed array, then a Proxy, must return what the full chain returns at every step.
- The existing 234-row lazy-array matrix (native/shadow roots × auto/tape/direct parsers × normal/scheduled/full-GC) must stay byte-identical.
- Code size is the trap: perf(codegen): serve lazy JSON array reads from the indexed inline cache #10114 measured that inlining a tier at every read site cost unrelated rows up to 5% through layout alone, and that the same cost relocates between rows when unrelated changes land. Whatever this emits must be measured for emitted-size growth, not only for speed.
Refs #10114 (added the lazy tier this would reorder), #10098, #793.
Activity
proggeramlug commentedon Sep 13, 2026
Closed by #10136 ("Closes #10118"), landed via merge train #10188 (main
5cec2fbbc9).