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
Whole-block in-place promotion (#7744/#7888) made a promoting copying minor cost 8.9 ns/object. It cannot apply to the first copying minor of a process,
because the policy decides from the previous cycle's measured young-survival
ratio and there is no previous cycle. Cycle 0 therefore evacuates
object-by-object — and on the live-set benchmarks it is the single largest GC
cost that remains.
Measured on the pinned quiet M1 mini (PERRY_GC_TRACE=1, per-cycle records,
recorded in gc-handoff/RETAIN4-NOTES.md):
program
cycle 0 share of all GC pause
retain
58% (30.2 ms of 52.0 ms; 245,752 objects / 17.7 MB copied)
retain1
81%
retain_wide1
71%
The copy buys nothing but the survival ratio: cycle 1 promotes those very
blocks in place.
Why the obvious fix is not obviously right
The nursery cap (gc_scavenge_nursery_cap_bytes, 16 MB) sets cycle 0's size, and PERRY_GC_SCAVENGE_NURSERY_MB already dials it. Measured with that knob on retain:
cap
cycle-0 pause
measured surv‰
total GC
16 MB (default)
30.2 ms
992
52.0 ms
4 MB
8.4
972
38.7
2 MB
5.2
958
30.7
1 MB
3.6
937 — below the 950 in-place threshold
161.7 (+ a 115 ms full)
Two hazards, both measured, that any design here has to answer:
Programs that currently never collect start collecting, and some collect
catastrophically.asyncpipe runs 0 cycles at 16 MB. At 2 MB it runs 4,
costing 385 ms on a 127 ms program (one 122 ms non-copying minor that
handles zero objects, one 248 ms copying minor at 1,646 ns/object). At 8 MB it
still runs 2, costing 266 ms. Its peak in-use sits just under 16 MB, so today
it is protected by luck.
Earlier promotion re-times the old_gen_bytes full-collection trigger. retain_wide1 at 2 MB takes two full mark-sweeps (85 + 114 ms) it does not
take at 16 MB — 424 ms of GC against 40 ms. retain_wide +63%, push_cls
+89%, churn_alloc +63%, interp +35%, iso_miss +22%.
A warm-up ramp (cap base>>3 → base over the first three cycles) avoids the
steady-state half of both hazards but not hazard 1 — asyncpipe still takes
its first two collections — and sizing it needs a corpus-wide run per candidate
ramp.
Alternatives worth pricing before picking one
A speculative first promotion: promote cycle 0 in place on the assumption
it is live, with the existing untraced-promotion budget as the bound, and let
the measured ratio correct it from cycle 1. The cost of being wrong is one
nursery of retained garbage, which is what the steady-state policy already
accepts.
Whatever lands must show, on the same host and corpus: cycle 0's pause down on
the retain* cluster, asyncpipe still at zero collections, and no
regression in full-collection count on retain_wide1/push_cls/churn_alloc.
A gc_ratchet pass, and — per CLAUDE.md — a gate that asserts its subject was
live (a promoting or skipped cycle 0 actually happened), not merely that nothing
threw.
Split out of #7742, which closed with the promotion tax it was opened for
measured at 8.9 ns/object.
Summary
Whole-block in-place promotion (#7744/#7888) made a promoting copying minor cost
8.9 ns/object. It cannot apply to the first copying minor of a process,
because the policy decides from the previous cycle's measured young-survival
ratio and there is no previous cycle. Cycle 0 therefore evacuates
object-by-object — and on the live-set benchmarks it is the single largest GC
cost that remains.
Measured on the pinned quiet M1 mini (
PERRY_GC_TRACE=1, per-cycle records,recorded in
gc-handoff/RETAIN4-NOTES.md):retainretain1retain_wide1The copy buys nothing but the survival ratio: cycle 1 promotes those very
blocks in place.
Why the obvious fix is not obviously right
The nursery cap (
gc_scavenge_nursery_cap_bytes, 16 MB) sets cycle 0's size, andPERRY_GC_SCAVENGE_NURSERY_MBalready dials it. Measured with that knob onretain:Two hazards, both measured, that any design here has to answer:
catastrophically.
asyncpiperuns 0 cycles at 16 MB. At 2 MB it runs 4,costing 385 ms on a 127 ms program (one 122 ms non-copying minor that
handles zero objects, one 248 ms copying minor at 1,646 ns/object). At 8 MB it
still runs 2, costing 266 ms. Its peak in-use sits just under 16 MB, so today
it is protected by luck.
old_gen_bytesfull-collection trigger.retain_wide1at 2 MB takes two full mark-sweeps (85 + 114 ms) it does nottake at 16 MB — 424 ms of GC against 40 ms.
retain_wide+63%,push_cls+89%,
churn_alloc+63%,interp+35%,iso_miss+22%.A warm-up ramp (cap
base>>3 → baseover the first three cycles) avoids thesteady-state half of both hazards but not hazard 1 —
asyncpipestill takesits first two collections — and sizing it needs a corpus-wide run per candidate
ramp.
Alternatives worth pricing before picking one
it is live, with the existing untraced-promotion budget as the bound, and let
the measured ratio correct it from cycle 1. The cost of being wrong is one
nursery of retained garbage, which is what the steady-state policy already
accepts.
born-old admission for the accumulating cohort. perf(gc): allocation-site pretenuring — long-lived cohorts are copied twice (Eden→survivor→old) #7598's post-mortem says why
the static/HIR form of this failed before, and
retain's accumulator isfunction-local, which that infrastructure's
region_runs_oncerule refuses.Acceptance
Whatever lands must show, on the same host and corpus: cycle 0's pause down on
the
retain*cluster,asyncpipestill at zero collections, and noregression in full-collection count on
retain_wide1/push_cls/churn_alloc.A
gc_ratchetpass, and — per CLAUDE.md — a gate that asserts its subject waslive (a promoting or skipped cycle 0 actually happened), not merely that nothing
threw.
Split out of #7742, which closed with the promotion tax it was opened for
measured at 8.9 ns/object.