Skip to content

perf(gc): allocation-site pretenuring — long-lived cohorts are copied twice (Eden→survivor→old) #7598

Description

@proggeramlug

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

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