You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 to js_lazy_array_index_probe
GC_TYPE_ARRAY → straight to arrlike.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.
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.
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.parsearray — 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].idloop in the JSON access benchmark (688 IR instructions, from--trace llvmon the post-#10114 compiler):.id)tav.*)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:repeatis 116.7 retired instructions perrows[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_typeon prime, and branch on it first:GC_TYPE_LAZY_ARRAY→ straight tojs_lazy_array_index_probeGC_TYPE_ARRAY→ straight toarrlike.ic.array_guardtav.*tierThis 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
GC_TYPE_LAZY_ARRAYas a distinct kind entirely, at which point this dispatch has one fewer arm but the same shape.Validation it needs
Refs #10114 (added the lazy tier this would reorder), #10098, #793.