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
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
Profile a single ai-astar iteration with perf record -g and confirm time is in Interpreter::gc_collect / mark-sweep, not in allocation.
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.
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
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
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):ai-astarhash-mapGeomean 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
ai-astariteration withperf record -gand confirm time is inInterpreter::gc_collect/ mark-sweep, not in allocation.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-livedNodeand queue entries swamp full heap scanshash-map— 1.52x today; should reach ≥ 1.8x — iterator and entry objects churn on every.entrySet()/.iterator()round-tripBox2D— 1.79x today; secondary lift — physics step allocates many short-lived contact / manifold objectssplay— 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 angleearley-boyer— 1.70x today; parser allocates short-lived AST nodes by the thousand; lift to ≥ 2.0x plausibleregexp-octane— currently 5.97s/iter on HEAD (was timeout on baseline); regex match results and capture arrays are short-livedtest262 (slow staging tests):
staging/sm/JSON/parse-mega-huge-array.js— 1.07x today; large array of objects, lots of intermediate allocationstaging/sm/expressions/nullish-coalescing.js— 1.37x today; intermediate value churn in a hot loopstaging/sm/Array/toSpliced-dense.js— 1.69x today; allocates new dense arrays pertoSplicedcallNegligible / 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 setsRelated
malloc/freeoverhead per allocation; nursery attacks the sweep cost per collection. Both are needed for benchmarks with large stable live sets.Out of scope (for now)
Concurrent or incremental GC, write barriers beyond the minimum needed for old → young, and full moving compaction.