Skip to content

Perf: generational GC nursery to cut sweep cost on large live sets #126

Description

@pmatos

Summary

After the GC threshold work in 845ad42 (decoupled pressure detection, adaptive byte-aware threshold, loop-backedge safepoints), GC frequency is well-controlled but per-collection sweep cost is still O(live). On benchmarks that hold a large stable working set while doing rapid allocation churn, this dominates. The pattern shows up clearly in the HEAD vs 7a4d095 perf comparison: the worst macro-result is on a benchmark with a ~1.8 GB live set.

A young-generation nursery would let most allocations die without ever being scanned by a full collection, addressing the cost axis (which #66's arena attacks the per-object allocation axis but does not fix).

Evidence

From a HEAD vs 7a4d095 perf comparison (/tmp/perf-report.md):

Benchmark Speedup HEAD/baseline Resident peak Note
JetStream ai-astar 1.11x ~1.8 GB Worst non-blocked JetStream entry
JetStream hash-map 1.52x ~1.9 GB (avg) Bucket churn in JS-level Map

Geomean across the rest of JetStream is 1.93x, so these are the visible outliers along with the array benchmarks (tracked separately in #125).

Hypothesis

Both benchmarks have the profile "large stable working set + steady allocation churn": A* keeps its open/closed sets alive for the whole search; hash-map keeps its bucket arrays alive for the whole iteration. Each adaptive-threshold-triggered collection still has to mark and sweep the entire heap, even though almost all freshly allocated objects (search nodes / iterator boxes) are unreachable by the next safepoint.

A two-generation collector with a small nursery (e.g. 4 MiB) would let those short-lived allocations be reclaimed by a copying scavenge that touches only the survivors, instead of by a mark-sweep over the whole heap.

Suggested experiments

  1. Profile a single ai-astar iteration with perf record -g and confirm time is in Interpreter::gc_collect / mark-sweep, not in allocation.
  2. Cheaper interim before generational: a per-size-class freelist so sweep does not have to clear-and-rebuild — would reduce constant factor without changing asymptotic behaviour.
  3. Generational design (preferred long-term): nursery for fresh allocations, promotion on survival of N collections, write barrier on old → young pointers. The existing root-set machinery in gc.rs (with ephemeron support) already covers most of what a tenured collector needs.

Expected beneficial impact

Tests where this should produce a measurable speedup (current HEAD vs baseline 7a4d095 numbers):

JetStream (clearest signal):

  • ai-astar — 1.11x today (worst non-blocked entry); should reach ≥ 1.6x — short-lived Node and queue entries swamp full heap scans
  • hash-map — 1.52x today; should reach ≥ 1.8x — iterator and entry objects churn on every .entrySet()/.iterator() round-trip
  • Box2D — 1.79x today; secondary lift — physics step allocates many short-lived contact / manifold objects
  • splay — was the canonical motivating benchmark for Perf: arena allocator for JS objects #66 with 36% in malloc/free; nursery would attack the same workload from the GC-cost angle
  • earley-boyer — 1.70x today; parser allocates short-lived AST nodes by the thousand; lift to ≥ 2.0x plausible
  • regexp-octane — currently 5.97s/iter on HEAD (was timeout on baseline); regex match results and capture arrays are short-lived

test262 (slow staging tests):

  • staging/sm/JSON/parse-mega-huge-array.js — 1.07x today; large array of objects, lots of intermediate allocation
  • staging/sm/expressions/nullish-coalescing.js — 1.37x today; intermediate value churn in a hot loop
  • staging/sm/Array/toSpliced-dense.js — 1.69x today; allocates new dense arrays per toSpliced call

Negligible / not expected to help:

  • bench_loop, bench_object, bench_json (allocate little)
  • bench_string (string-concat is now Arc-backed, not allocation-dominated)
  • bench_regex, bench_fib (compute-bound)
  • octane-code-load, bigint-bigdenary, crypto, stanford-crypto-*, unicode-class-braced (compute-bound)
  • delta-blue, richards, UniPoker — already fast on HEAD; small live sets

Related

Out of scope (for now)

Concurrent or incremental GC, write barriers beyond the minimum needed for old → young, and full moving compaction.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions