Skip to content

perf(gc): the minor collector still does a full-region sweep walk over the old generation (#6181 sub-cost a) #7716

Closed

Description

@proggeramlug

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:

Before coding

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.

Activity

  1. added
    performanceRuntime, compile-time, build-size, or memory performance
    on Aug 9, 2026
  2. proggeramlug commented on Aug 10, 2026

    @proggeramlug
    ContributorAuthor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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