Found while measuring #7916/#7714 (PR #7928, INLINE_SLOT_FLOOR 4 → 2).
The mismatch
The nursery/promotion budget is denominated in bytes. The collector's per-cycle cost is
dominated by per-object work (copy, forwarding, layout transfer, page registration —
~50–100 ns each). Those two are not the same quantity, and the gap opens exactly when object
representation gets more compact.
The measurement
Same program (gc-handoff/bench/retain1.ts, 1 M surviving 2-field literals), same commit,
one arm with 72-byte objects and one with 56-byte objects. Every minor fires at the same
byte mark and processes the same bytes:
| minor |
bytes, 72 B objects → 56 B objects |
objects, 72 B → 56 B |
| 1 (copy) |
17 694 064 → 17 694 216 |
245 752 → 315 969 |
| 2 (promote) |
18 742 672 → 18 742 816 |
260 316 → 334 694 |
| 3 (promote) |
34 601 760 → 34 602 008 |
480 580 → 617 893 |
The object ratio is 1.286 = 72/56, exactly. deeplist reproduces it to three decimals.
Consequences on those two rows: GC pause 39.60 → 50.10 ms (+26.5%), and that +10.5 ms
exceeds the whole program's cycle delta (25.2 M cycles ≈ 7.9 ms) — the mutator got faster
and the collector got slower. Cost per extra promoted object: 10.5 ms / 211 691 = 49.6 ns,
i.e. the per-object price did not change at all. Only the population inside a fixed byte
budget did.
Why it matters beyond one PR
Object representation is the current perf direction (#7916 established that the collector is
no longer the binding constraint on the retain cluster; the mutator is). But every byte
removed from an object increases the number of objects the collector handles per unit of its
own budget. A byte-denominated trigger in front of a per-object cost model makes "make
objects smaller" and "make the collector do more work per cycle" the same action, so the
footprint work partially pays for itself in GC time until this is fixed.
What "fixed" would mean
An object-count term alongside the byte term in the nursery/promotion budget, so a cycle's
work — not its bytes — is what is being paced. Adjacent to #7715 (B4 generation bit) and
#7432 (adaptive tenuring young cap, which is the valve reading eden_live_bytes here).
Caveat that keeps this honest
Total promotion work is set by the surviving object count, which does not change — 1 M either
way. retain1 promotes 740 896 of 1 M objects with 72-byte objects and 952 587 with 56-byte
ones, so the smaller representation pulls ~211 k promotions forward into the measurement
window rather than creating them. A program that runs to completion pays the same total.
That makes the effect largest on exit-bounded microbenchmarks — which is most of the perf
corpus, so it is still worth pacing correctly.
Reproduce: PERRY_GC_TRACE=1 PERRY_GC_DIAG=1 ./p_retain1 on either side of PR #7928 and
compare the [gc-copy-minor] ran … lines. Full write-up: gc-handoff/REPR-NOTES.md §3d.
Found while measuring #7916/#7714 (PR #7928,
INLINE_SLOT_FLOOR4 → 2).The mismatch
The nursery/promotion budget is denominated in bytes. The collector's per-cycle cost is
dominated by per-object work (copy, forwarding, layout transfer, page registration —
~50–100 ns each). Those two are not the same quantity, and the gap opens exactly when object
representation gets more compact.
The measurement
Same program (
gc-handoff/bench/retain1.ts, 1 M surviving 2-field literals), same commit,one arm with 72-byte objects and one with 56-byte objects. Every minor fires at the same
byte mark and processes the same bytes:
The object ratio is 1.286 = 72/56, exactly.
deeplistreproduces it to three decimals.Consequences on those two rows: GC pause 39.60 → 50.10 ms (+26.5%), and that +10.5 ms
exceeds the whole program's cycle delta (25.2 M cycles ≈ 7.9 ms) — the mutator got faster
and the collector got slower. Cost per extra promoted object: 10.5 ms / 211 691 = 49.6 ns,
i.e. the per-object price did not change at all. Only the population inside a fixed byte
budget did.
Why it matters beyond one PR
Object representation is the current perf direction (#7916 established that the collector is
no longer the binding constraint on the retain cluster; the mutator is). But every byte
removed from an object increases the number of objects the collector handles per unit of its
own budget. A byte-denominated trigger in front of a per-object cost model makes "make
objects smaller" and "make the collector do more work per cycle" the same action, so the
footprint work partially pays for itself in GC time until this is fixed.
What "fixed" would mean
An object-count term alongside the byte term in the nursery/promotion budget, so a cycle's
work — not its bytes — is what is being paced. Adjacent to #7715 (B4 generation bit) and
#7432 (adaptive tenuring young cap, which is the valve reading
eden_live_byteshere).Caveat that keeps this honest
Total promotion work is set by the surviving object count, which does not change — 1 M either
way.
retain1promotes 740 896 of 1 M objects with 72-byte objects and 952 587 with 56-byteones, so the smaller representation pulls ~211 k promotions forward into the measurement
window rather than creating them. A program that runs to completion pays the same total.
That makes the effect largest on exit-bounded microbenchmarks — which is most of the perf
corpus, so it is still worth pacing correctly.
Reproduce:
PERRY_GC_TRACE=1 PERRY_GC_DIAG=1 ./p_retain1on either side of PR #7928 andcompare the
[gc-copy-minor] ran …lines. Full write-up:gc-handoff/REPR-NOTES.md§3d.