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
{{ message }}
Repository navigation
perf(gc): the minor collector still does a full-region sweep walk over the old generation (#6181 sub-cost a) #7716
The one unfixed sub-cost from #6181, refiled on its own so it stops sharing a ticket with two that are done.
Fixed and closed with #6181: (b) the whole-heap remembered-set rebuild and (c) the per-cycle page-meta iterations, via #6213 / #6277 / #6568.
Still live: sub-cost (a) — the minor collector performs a full-region sweep walk over the old generation.crates/perry-runtime/src/gc/oldgen.rs::sweep (line ~476) is reached on the minor path, so minor-GC cost scales with old-gen size rather than with nursery survivors — which is the property a generational collector exists to avoid.
Why this is worth doing and what to watch
The remembered set already exists precisely so a minor need not retrace the old generation, so the walk is redundant work on the hot path. But two things make this less mechanical than it looks:
The malloc sweep is a separate path (sweep_malloc_objects, copied_minor_malloc_sweep_due) and is gated by trigger kind. Do not conflate the two — a change that skips one and not the other will look like it works.
Re-measure. #6181's figures predate #6213/#6277/#6568, so the remaining share attributable to (a) alone has never been isolated. Use PERRY_GC_DIAG=1 and confirm the cycle KINDS you are measuring are the ones you think — census counters cover copying minors ONLY, and a run whose cycles are full mark-sweeps says nothing about the minor path.
Bench host per the standing rule: the quiet M1 mini.
Re-measured first, as this issue instructs. Sub-cost (a) is 0.0 ms on the shipped default path. The premise — "minor-GC cost scales with old-gen size rather than with nursery survivors" — no longer holds on main @ db44b31b7.
Method: release build, PERRY_GC_TRACE=1, per-cycle phase_us split by collection_kind, on three workloads with very different old-gen sizes. Cycle kinds were checked rather than assumed, per the warning in this issue.
Default configuration (loop polls ON since #7721 — copying minors)
workload
minors
minor pause
sweep share of minor pause
retain_live_big (large live old gen)
15
1,739.0 ms
0.0 ms (0.0%)
churn
105
21.1 ms
0.0 ms (0.0%)
tree
0 minors (40 fulls)
—
—
Where minor pause actually goes on retain_live_big:
copying_nursery 1553.9 ms
in_place_promotion 129.4 ms
sweep 0.0 ms
The sweep key is not missing from the accounting — the same runs attribute 68.0 ms of sweep to their full cycles, and tree's fulls are 53.7% sweep. Minors simply do not spend time there.
The reason is structural: the minor path is now the copying minor, which evacuates the nursery and never walks the old region. That became the default via #7690 / #7721, after #6181 was written — which is precisely why this issue told the next person to re-measure.
The one place (a) still exists
Forcing the non-moving minor path with PERRY_GC_MOVING_LOOP_POLLS=0 on the same workload:
polls ON (default)
polls OFF (non-moving minors)
minors
15, all copying
13, none copying
total minor pause
1,739 ms
21,989 ms
sweep
0.0 ms (0.0%)
1,443.1 ms (6.6%)
atomic_finalize
—
13,715.8 ms (62.4%)
reference_rewrite
—
12,045.9 ms (54.8%)
So the old-gen sweep walk is reachable only on the non-moving minor path, where it is 6.6% of a path that is 12.6× slower in total for reasons that dwarf it. Optimising it would be optimising a configuration that is off by default and already pathological, and the two hazards this issue warns about — the separate malloc-sweep path, and the #7682/#7690 livelock shape where removing work from the minor path changes what the trigger arithmetic sees — would all still have to be paid to get there.
Closing as measured-moot. What would reopen it: a default-path workload whose minors show non-zero sweep in phase_us, or a decision to make the non-moving minor a supported steady-state configuration again rather than a bisection tool.
One caveat stated plainly: this measures phase attribution, not call reachability. oldgen::sweep may still be entered on a copying minor and return immediately; what the numbers refute is the cost claim, which is what the issue is about.
Activity
proggeramlug commentedon Aug 10, 2026
Re-measured first, as this issue instructs. Sub-cost (a) is 0.0 ms on the shipped default path. The premise — "minor-GC cost scales with old-gen size rather than with nursery survivors" — no longer holds on
main@db44b31b7.Method: release build,
PERRY_GC_TRACE=1, per-cyclephase_ussplit bycollection_kind, on three workloads with very different old-gen sizes. Cycle kinds were checked rather than assumed, per the warning in this issue.Default configuration (loop polls ON since #7721 — copying minors)
sweepshare of minor pauseretain_live_big(large live old gen)churntreeWhere minor pause actually goes on
retain_live_big:The
sweepkey is not missing from the accounting — the same runs attribute 68.0 ms of sweep to their full cycles, andtree's fulls are 53.7% sweep. Minors simply do not spend time there.The reason is structural: the minor path is now the copying minor, which evacuates the nursery and never walks the old region. That became the default via #7690 / #7721, after #6181 was written — which is precisely why this issue told the next person to re-measure.
The one place (a) still exists
Forcing the non-moving minor path with
PERRY_GC_MOVING_LOOP_POLLS=0on the same workload:sweepatomic_finalizereference_rewriteSo the old-gen sweep walk is reachable only on the non-moving minor path, where it is 6.6% of a path that is 12.6× slower in total for reasons that dwarf it. Optimising it would be optimising a configuration that is off by default and already pathological, and the two hazards this issue warns about — the separate malloc-sweep path, and the #7682/#7690 livelock shape where removing work from the minor path changes what the trigger arithmetic sees — would all still have to be paid to get there.
Closing as measured-moot. What would reopen it: a default-path workload whose minors show non-zero
sweepinphase_us, or a decision to make the non-moving minor a supported steady-state configuration again rather than a bisection tool.One caveat stated plainly: this measures phase attribution, not call reachability.
oldgen::sweepmay still be entered on a copying minor and return immediately; what the numbers refute is the cost claim, which is what the issue is about.