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
Follow-up to #7592 (fixed by #7594 + #7596 up to a constant factor). The quadratic is gone, but the dominant remaining cost on promote-heavy workloads is structural: every long-lived object is copied twice — Eden→survivor by the first copying minor, survivor→old by the next.
Measured shape (json_pipeline, 500k records, after #7596)
# kind trigger old_before eden_live promoted pause
3 minor arena_bytes 4.2 MB 268 MB 0 1,658 ms <- 268 MB Eden->survivor, ALL of it long-lived
4 minor arena_bytes 4.2 MB 1 MB 269 MB 2,223 ms <- the same bytes, survivor->old
3.9 s of the remaining 5.1 s build_out is these two copies of the same 268 MB. A collection-free run of the same loop is 764 ns/record vs the current ~23,000, so this is the bulk of what is left on #7592's workload.
Why the cheap fixes do not work (all measured or ruled out this campaign)
The survival-rate lock is a cycle too late by construction — it keys on the previous cycle's survivor round-trip, so the first big minor always pays the wasted copy. It did engage here (cycle 4 promoted), which is exactly why the waste is confined to cycle 3's copy.
An Eden-side lock (≥90 % of Eden survives ⇒ S=1) measured 1.0× — it also only takes effect on the next copying minor, and a one-burst workload has no next minor that matters.
A mid-cycle valve (promote directly once the to-survivor space overflows) is ruled out by gc: adaptive tenuring + young-scoped scavenge cap (fixes the large-live-set scavenge regression) #7432: which objects promote would then depend on root traversal order, breaking the bit-identical copied/promoted counters the gc-ratchet's determinism contract requires. The per-object decision must stay a pure function of (flags, age, cycle-start threshold).
Eden in-use at cycle start is not a survival signal — churn workloads also overshoot Eden, with ~0 % survival; gating S on it would mis-promote garbage.
The structural fix: allocation-site pretenuring
HotSpot-style: per-allocation-site survival feedback (site → promoted-fraction), fed back either as a runtime site tag on the allocation header or as a codegen hint (arena_alloc_gc_old directly for hot sites whose cohort demonstrably tenures). Deterministic per-cycle (the decision is made at allocation, not during the collection), so it composes with #7432.
Perry has unusually good static material for this compared to a JIT: out.push({...})-into-an-accumulator is visible in HIR (the #7469 loop-site work already classifies allocation sites by loop context, and collectors/all_pointer_arrays.rs proves which locals only receive freshly-allocated pushes). A static "allocated in a loop AND stored into an accumulator that outlives the loop" proof may cover the common case without any runtime feedback.
Acceptance
json_pipeline 500k build_out ≤ ~2.5 s (one copy of the live cohort instead of two) with output hash unchanged.
gc-ratchet: all semantic counters bit-identical on the 12 probes except deliberate, documented promotion-count changes on the promote-heavy probes.
Follow-up to #7592 (fixed by #7594 + #7596 up to a constant factor). The quadratic is gone, but the dominant remaining cost on promote-heavy workloads is structural: every long-lived object is copied twice — Eden→survivor by the first copying minor, survivor→old by the next.
Measured shape (json_pipeline, 500k records, after #7596)
3.9 s of the remaining 5.1 s
build_outis these two copies of the same 268 MB. A collection-free run of the same loop is 764 ns/record vs the current ~23,000, so this is the bulk of what is left on #7592's workload.Why the cheap fixes do not work (all measured or ruled out this campaign)
The structural fix: allocation-site pretenuring
HotSpot-style: per-allocation-site survival feedback (site → promoted-fraction), fed back either as a runtime site tag on the allocation header or as a codegen hint (
arena_alloc_gc_olddirectly for hot sites whose cohort demonstrably tenures). Deterministic per-cycle (the decision is made at allocation, not during the collection), so it composes with #7432.Perry has unusually good static material for this compared to a JIT:
out.push({...})-into-an-accumulator is visible in HIR (the #7469 loop-site work already classifies allocation sites by loop context, andcollectors/all_pointer_arrays.rsproves which locals only receive freshly-allocated pushes). A static "allocated in a loop AND stored into an accumulator that outlives the loop" proof may cover the common case without any runtime feedback.Acceptance
build_out≤ ~2.5 s (one copy of the live cohort instead of two) with output hash unchanged.