Skip to content

perf(gc): the nursery/promotion budget is denominated in BYTES but the collector's cost is per-OBJECT — compaction is taxed back #7929

Description

@proggeramlug

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    performanceRuntime, compile-time, build-size, or memory performance

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions