Split out of #7478's decomposition (see #7537, merged). This is now the dominant term in field_access, and it is not the tape's intrinsic cost.
The measurement
The public baseline's idiomatic row flips two switches at once (PERRY_JSON_TAPE=0 and PERRY_GEN_GC=0), so "tape on, mark-sweep" had never been measured. Decomposing all four combinations on the pinned quiet host (M1 mini, 11 runs, taskpolicy -t 0 -l 0) at origin/main @ fc5e56e:
| variant |
tape + genGC |
tape + markSweep |
| parse+scan |
2729 ms · σ 214.9 · 208 MB |
2957 ms · σ 8.8 · 66 MB |
| field_access |
2942 ms · σ 101.8 · 222 MB |
3247 ms · σ 7.7 · 67 MB |
Identical tape, different collector. σ collapses from 214.9 to 8.8 and RSS from 208 MB to 66 MB — a 3.2× memory difference and a 25× variance difference, with the tape doing exactly the same work.
Neither factor alone explains it: tape-off under gen-GC is σ 17.6. It is specifically the interaction.
Mechanism
Each parse allocates header + tape as one ~2.4 MB block (200,002 TapeEntrys for this fixture). That rounds up to a dedicated oversized arena block and is retained; the resulting old-gen growth fires full collections at data-dependent points, which is both the variance and the retained footprint.
Why it matters more after #7537
#7537 took field_access from 2981 ms to 2016 ms by flipping scans to the batch parser early, and parse+scan from 2729 to 1339. With the element-wise materializer no longer the bottleneck, this interaction is what is left: full - scan is 707 ms under gen-GC but only 309 ms under mark-sweep for the identical tree.
The strongest single data point: tape + markSweep field_access after #7537 is 1759 ms at σ 2.1 and 76 MB — within 282 ms of the idiomatic floor and completely stable. The same build under gen-GC is 2016 ms at σ 219 and 193 MB. The tape is not the problem any more; the collector's handling of its large per-parse block is.
Suggested direction
Treat a large, immediately-tenured, pointer-free block as a special case rather than as ordinary old-gen growth — it never needs scanning (a tape is TapeEntrys, not JSValues) and never needs copying. Options worth measuring: allocate it outside the arena entirely; mark the block pointer-free so full collections skip it; or exempt oversized single-allocation blocks from the trigger arithmetic that is currently firing fulls.
Acceptance: field_access under the default (gen-GC) configuration within noise of the tape + markSweep number — i.e. ~1760 ms at low σ — without regressing roundtrip (201 ms, the memcpy path).
Split out of #7478's decomposition (see #7537, merged). This is now the dominant term in
field_access, and it is not the tape's intrinsic cost.The measurement
The public baseline's
idiomaticrow flips two switches at once (PERRY_JSON_TAPE=0andPERRY_GEN_GC=0), so "tape on, mark-sweep" had never been measured. Decomposing all four combinations on the pinned quiet host (M1 mini, 11 runs,taskpolicy -t 0 -l 0) atorigin/main@ fc5e56e:Identical tape, different collector. σ collapses from 214.9 to 8.8 and RSS from 208 MB to 66 MB — a 3.2× memory difference and a 25× variance difference, with the tape doing exactly the same work.
Neither factor alone explains it: tape-off under gen-GC is σ 17.6. It is specifically the interaction.
Mechanism
Each parse allocates header + tape as one ~2.4 MB block (200,002
TapeEntrys for this fixture). That rounds up to a dedicated oversized arena block and is retained; the resulting old-gen growth fires full collections at data-dependent points, which is both the variance and the retained footprint.Why it matters more after #7537
#7537 took
field_accessfrom 2981 ms to 2016 ms by flipping scans to the batch parser early, andparse+scanfrom 2729 to 1339. With the element-wise materializer no longer the bottleneck, this interaction is what is left:full - scanis 707 ms under gen-GC but only 309 ms under mark-sweep for the identical tree.The strongest single data point:
tape + markSweepfield_access after #7537 is 1759 ms at σ 2.1 and 76 MB — within 282 ms of the idiomatic floor and completely stable. The same build under gen-GC is 2016 ms at σ 219 and 193 MB. The tape is not the problem any more; the collector's handling of its large per-parse block is.Suggested direction
Treat a large, immediately-tenured, pointer-free block as a special case rather than as ordinary old-gen growth — it never needs scanning (a tape is
TapeEntrys, not JSValues) and never needs copying. Options worth measuring: allocate it outside the arena entirely; mark the block pointer-free so full collections skip it; or exempt oversized single-allocation blocks from the trigger arithmetic that is currently firing fulls.Acceptance:
field_accessunder the default (gen-GC) configuration within noise of thetape + markSweepnumber — i.e. ~1760 ms at low σ — without regressingroundtrip(201 ms, the memcpy path).