Skip to content

GC roots: statepoints only — make statepoint lowering linear and remove the shadow-frame size path (#11960) #12023

Closed

Description

@proggeramlug

Decision (owner)

GC roots are lowered with LLVM statepoints, and only statepoints. Shadow-frame homes are the legacy gcroot/shadow-stack design. #11960 (#11926) brought them back as a second rooting regime for root-heavy functions, chosen by a size heuristic. That is a second path and it is being removed now, not deferred. The quadratic code growth it worked around gets fixed inside statepoint lowering.

Problem

Perry promotes managed locals to SSA before RS4GC, so every live GC value is relocated at every statepoint. Statepoint lowering spills all of them before each call and reloads fresh relocated values after it, and the spill slots are evidently not reused across statepoints. Results:

  • the big() witness grows quadratically: 38.7 KB (25 locals), 121 KB (50), 500 KB (100), 1.83 MB (200);
  • Claude Code's GW7 was 26.6 MB of code with a ~2.3 MB stack frame, 62% of it stack-slot copy runs;
  • across the cc binary, copy runs were ~15% of instructions.

#11960 routes functions with ≥25 safepoints, >2× more root slots than safepoints and an estimate >2,048 relocations to shadow frames (root_home_size_candidate, DEFAULT_ROOT_HOME_RELOCATIONS in crates/perry-codegen/src/codegen/helpers.rs). The older #8620 32M-relocation ceiling does the same as a compile-time backstop.

Goal

  1. Linear statepoint lowering. Code size and compile time grow linearly with live values + calls. A GC value that sits in a stack slot across consecutive statepoints stays there, and the stack map names the slot; it is not re-spilled and reloaded around every call. Candidate mechanisms, to be prototyped and measured rather than assumed:
    • LLVM statepoint spill-slot reuse across blocks;
    • reporting long-lived locals as stack-slot locations (allocas in the gc-live bundle, if StatepointLowering supports them on our LLVM 22);
    • max-registers-for-gc-values;
    • fewer GC values: proven numbers are not GC pointers, and recompute instead of relocate where possible.
  2. Delete the shadow-frame size path (root_home_size_candidate and its constants) in the same change. Re-evaluate the codegen: #8589 root-spill default threshold (4M) too low — spills moderate-fan-out functions and slows their compile #8620 ceiling, and delete it too if compile time is linear.
  3. Witness sizes at or below perf(codegen): use stable homes for root-heavy functions (#11926) #11960's shadow-frame numbers (q200 ≤ ~249 KB), with no runtime regression.

Acceptance

Activity

  1. proggeramlug commented on Oct 5, 2026

    @proggeramlug
    ContributorAuthor

    Scope (owner): shadow frames stay ONLY where a target platform requires them, i.e. targets where LLVM statepoints/stack maps are unavailable or unsupported. Everywhere else they come out completely: the #11960 size heuristic, the #8620 32M-relocation ceiling, and any other non-platform fallback (env-selected, optnone, per-function compile-time escapes). The lane inventories every shadow-frame use, classifies each as platform-required or not with a reason, and removes all of the 'not' ones in this change.

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions