perf(gc): whole-block in-place promotion of a fully-live young generation (#7742) - #7744
Conversation
0d3b0e8 to
dad34f3
Compare
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (4)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughThe runtime adds measured whole-block in-place promotion for highly live young-generation blocks. It retags blocks, indexes live objects, integrates promoted objects into copying collection, updates pacing and telemetry, and adds policy and end-to-end tests. ChangesWhole-block in-place promotion
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
crates/perry-runtime/src/gc/tests/promote_in_place.rs (1)
45-63: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winPin the intended promotion threshold.
Lines 49-60 accept every threshold from 5 through 999. A change from the measured boundary to 5 would pass this test and promote nearly every non-empty young generation. Assert the selected permille value, or test policy decisions on both adjacent boundary values.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/perry-runtime/src/gc/tests/promote_in_place.rs` around lines 45 - 63, Strengthen threshold_separates_the_measured_workload_population by pinning the intended PROMOTE_SURVIVAL_THRESHOLD_PERMILLE value or explicitly testing policy behavior at both adjacent boundaries. Ensure the test fails if the threshold is changed to an overly permissive value such as 5, while preserving the fully_live and churny classification checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@changelog.d/7744-gc-whole-block-in-place-promotion.md`:
- Line 5: Update the retain.ts baseline value in the changelog benchmark summary
from 0.80 s to 0.81 s, matching the corresponding benchmark entry later in the
file and the PR description; leave the reported result and retain_wide.ts values
unchanged.
In `@crates/perry-runtime/src/arena/promote.rs`:
- Around line 36-38: Update the module documentation near
retag_young_for_in_place_promotion to state that it flips every in-use Eden
block and blocks from both survivor semispaces to generation Old in
HeapSpace::PromotedYoung. Keep the wording consistent with the implementation’s
unconditional [0usize, 1usize] iteration and the resulting no-Nursery invariant
used by skip_remembering.
In `@crates/perry-runtime/src/gc/copying.rs`:
- Around line 446-447: Correct the function reference in the documentation
comment near the copying optimization to use
debug_assert_no_remembering_possible, matching the defined and called function
name; leave the surrounding explanation unchanged.
In `@crates/perry-runtime/src/gc/tests/promote_in_place.rs`:
- Around line 84-90: Update an_unmeasured_thread_never_promotes to explicitly
exercise the absent-measurement state by adding a test helper that clears
LAST_YOUNG_SURVIVAL_PERMILLE and invoking it before
should_promote_young_in_place. Keep the assertion that promotion is rejected,
while ensuring the test no longer initializes the state with
InPlacePromotionTestGuard::enabled(0).
---
Nitpick comments:
In `@crates/perry-runtime/src/gc/tests/promote_in_place.rs`:
- Around line 45-63: Strengthen
threshold_separates_the_measured_workload_population by pinning the intended
PROMOTE_SURVIVAL_THRESHOLD_PERMILLE value or explicitly testing policy behavior
at both adjacent boundaries. Ensure the test fails if the threshold is changed
to an overly permissive value such as 5, while preserving the fully_live and
churny classification checks.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: ef7cacb6-e9f7-4d27-a55a-bd14f254ceae
📒 Files selected for processing (14)
changelog.d/7744-gc-whole-block-in-place-promotion.mdcrates/perry-runtime/src/arena/mod.rscrates/perry-runtime/src/arena/page_meta.rscrates/perry-runtime/src/arena/promote.rscrates/perry-runtime/src/arena/tests.rscrates/perry-runtime/src/gc/copying.rscrates/perry-runtime/src/gc/layout.rscrates/perry-runtime/src/gc/mod.rscrates/perry-runtime/src/gc/policy.rscrates/perry-runtime/src/gc/promote_in_place.rscrates/perry-runtime/src/gc/telemetry.rscrates/perry-runtime/src/gc/tests/mod.rscrates/perry-runtime/src/gc/tests/promote_in_place.rsscripts/addr_class_allowlist.txt
…tion (#7742) When a copying minor's nursery is (near-)entirely live, relabel its blocks as old-gen instead of evacuating them object by object. `retain.ts` 0.80 s -> 0.53 s, `retain_wide.ts` 1.33 -> 1.07, `deeplist` 0.30 -> 0.24 (RSS 117 -> 97 MB); promotion cost 243 ns/object -> 101 ns/object, derived from the trace's promoted-object count and pause. The decision is a measurement, not a guess: the young-survival ratio is bimodal across the GC benchmark set (1.000 on retain/deeplist, <= 0.004 on churn/push/cycles), and a PROMOTING cycle still traces, so it re-measures the ratio it will be judged by. A misprediction therefore costs one nursery of retained garbage, and a running 32 MB cap bounds the steady state. Full rationale, the pacing regression this had to fix first, and the three remembered-set passes a promoting cycle can prove empty: see the changeset.
…eal unmeasured-state test
49c6f58 to
7d89800
Compare
Merging as v0.5.1434 — I reproduced the soundness evidence independentlyThe risk here is concentrated in one place: this skips three remembered-set passes on a promoting cycle. If the "after the retag no address classifies as So I built a retain-shaped workload of my own (3 M records, nothing ever dropped) and ran the instrument that speaks to exactly that:
A/B on my own host (loaded dev box, so absolute numbers differ from the pinned mini, but same-host comparison is valid): 1.35 s → 0.57 s, corroborating the reported 0.81 → 0.53 in direction and magnitude. What makes the design defensible rather than a gamble
The honesty I want to keepThree limits stated rather than glossed:
|
Closes part of #7742.
When a copying minor's nursery is (near-)entirely live, relabel its blocks as
old-gen instead of evacuating them object by object.
retainretain_wideretain1retain_wide1deeplistchurn/churn_alloc/push_clspush_num/churn_read/cyclestree/tree_wideBest-of-5 wall clock on the pinned quiet M1 mini (load ~1.6), interleaved,
stdout byte-verified against
node --experimental-strip-typesbefore timing.Promotion cost per object 243 ns → 101 ns, derived from the GC trace's own
promoted-object count and pause.
Why the old path was pure overhead here
retain.tsbuilds a 3M-element array of records and drops none. Its tracemeasures a young-survival ratio of 1.000 on every copying minor, and its
four promoting minors moved 2,097,155 objects in 509.7 ms. Each move paid for a
fresh
arena_alloc_gc_old, amemcpy, alayout_transfer,old_page_account_promoted_object, the move hooks, a forwarding stub, and therewrite of every referring slot — to put an object somewhere it had no reason
to be.
Design notes worth reviewing
trace and Eden blocks are recycled at offset 0, so it is per cycle from the
previous cycle's ratio. What makes that sound is that a promoting cycle
still traces, so it re-measures — a workload that flips from live to
garbage pays one nursery of retained garbage and the policy turns itself off.
A running 32 MB cap bounds the steady state the per-cycle correction misses.
The measured population is bimodal by three orders of magnitude (1.000 on
retain/deeplist, ≤ 0.004 on churn/push/cycles).
Old ⟹ TENURED(perf(gc): write barriers cost 16% on an all-numeric store workload — elide on provably-non-pointer stores #7511) is upheld by stamping every header on a promotedblock, dead ones included, and asserted end to end.
proof rather than a heuristic: after the retag no address classifies as
Nursery(every in-use young block was taken), so with an empty mallocregistry there is no child any of them could remember.
debug_assert_no_remembering_possiblere-derives it from the heap. Most ofthe win is here.
regression: promotion hands Eden's blocks away, so the arena-bytes trigger's
runway collapses. The capacity is returned as one-shot trigger headroom
rather than as re-reserved blocks (the eager form fixes pacing too but maps
memory the program may never reach —
retain_widepeak RSS 470 vs 447 MB).classify_arenaresolved the page-generation range twice per visitedslot; it now resolves once and answers the header's classification from the
range base, which is also the guard that keeps a garbage candidate at a range
start from reading the unmapped page below.
Validation
cargo test -p perry-runtime --release: 1982 passed, 0 failed.gc-handoff/apps/iso_miss.ts→checksum 437840 misses 0, plain and underPERRY_GC_PROTECT_FROMSPACE=1 PERRY_GC_PROTECT_FROMSPACE_DEPTH=800andPERRY_GC_VERIFY_EVACUATION=1. It is an interpreter, so it takes theordinary copying path on all 50 of its minors and still retires 50 quarantine
sets — the instrument's coverage is unchanged, and it does not cover the new
path.
PERRY_GC_FROMSPACE_SCAN_ABORT=1 ./n_retainover 4 promoting cycles (up to 2,111,418 objects / 21,085,616 words per
cycle) reports
missing_rewrites=0 dangling=0 never_dirty=0 lost_dirty=0 dirty_but_missed=0on every cycle — the last three are exactly the "theremembered set is missing an edge" counters the skipped passes could break.
gc_ratchet(7 repeats,shared_ci): every gated semantic cell isidentical to
origin/mainmeasured on the same host,12_large_live_setincluded. The six cells it reports as REGRESSION are reported identically by
origin/mainitself — pre-existing drift against the pinned artifact. Thecorollary is that no ratchet probe runs a promoting cycle, so the ratchet does
not yet cover this path.
check_file_size.sh,addr_class_inventory.py,cargo fmt --all -- --checkall clean.
Not closed
retainis still 4.4× Node andretain_wide7.1×, so #7742 stays open. Theresidue is no longer promotion bookkeeping: on a
retain_live_bigprofile theremaining GC time is the remembered-set scan over the array's dirty element
slots (26%) and one full collection that reclaims nothing (28%).
Summary by CodeRabbit
New Features
Performance
Tests