Repository navigation
Description
The 2026-07-31/08-01 stackmap campaign (#7108, branch exp/stackmap-viability, full write-up in docs/statepoint-gc-experiment.md) ends with every road to native-root file-size parity converging on one experiment that has not been run: RewriteStatepointsForGC over managed-pointer SSA.
Why this is the only remaining road, now demonstrated rather than argued:
- The explicit statepoint bridge is correct and speed-competitive (quiet-host geo −1.1% vs shadow, RSS flat) but its metadata measures 3.83 MB against 150 KB of text recovered on the real
test-drizzle-pgapp (~25×) after a −59% reduction campaign (audited call effects, noreturn elision, safepoint contract). The remaining records are dominated by base/derived pairs and per-safepoint spill slots that explicit emission cannot avoid. - The cheap alternative was built and killed: a compact per-function metadata mode measured 10–13× smaller and was deleted as a proven negative result (
bd066d62b, deleted ata95f59e96). A ten-line churn loop corrupts deterministically under moving minors, bit-identically even with the walker's visits disabled — the mutator reads from-space through stale heap-derived values in optimized SSA. The same module carries 79gc.relocates under the statepoint backend. Design law: root metadata without relocation semantics is unsound under an optimizing compiler. Per-call plain maps only kept the window small enough for probes to pass (that mode is also deleted — itsRegister R#Nlocations are unrecoverable by construction).
What the experiment is (#7108 costed it as option 1, "weeks"):
- Represent GC-capable values as
ptr addrspace(1)in SSA — the whole-NaN-box carrier is already proven (Stack maps vs the codegen shadow stack: viability experiment (Q1 passes; 0.44 MB text saved for 4.5-16.6 MB metadata) #7108 Q1:bitcast double → i64 → inttoptr, folds to zero instructions, relocated end-to-end at runtime). - Tag functions
gc "statepoint-example", letopt -passes=rewrite-statepoints-for-gcinsert statepoints, results, relocates, and downstream-use rewrites — deleting the hand-emission and its conservative CFG-union liveness. - Toolchain: requires
opt(Homebrew LLVM; Xcode ships none). Verified working end-to-end in Stack maps vs the codegen shadow stack: viability experiment (Q1 passes; 0.44 MB text saved for 4.5-16.6 MB metadata) #7108. Gate the mode onoptpresence; this is the distribution decision that issue named, scoped here to research.
Acceptance: the existing gate stack (8 probes × forced evacuation × verify-walker vs pinned Node, statepoint-report deltas, quiet-host matrix) plus the real-app criterion that defines success: __llvm_stackmaps + __text delta vs shadow ≤ 0 on test-drizzle-pg — i.e., LLVM-computed liveness (dead-root pruning is free in RS4GC) must close what audited elision provably cannot.
Interaction: repsel promotion (#7034 line) independently shrinks the recorded set — every value proven non-pointer deletes its records in either backend. The two compose; neither replaces the other.
Activity
research(gc): first RS4GC pipeline slice (PERRY_RS4GC, #7174) - 5/8 p…
proggeramlug commentedon Aug 1, 2026
Status update: the first pipeline slice exists on
exp/stackmap-viabilityat503ecb7e9— this issue now has running code and failing tests instead of a costed design.Working: root-alloca surgery to
ptr addrspace(1)(fail-closed per function, bail to the explicit statepoint backend on any unrecognized use shape),gc "statepoint-example"tagging, audited non-collecting callees as"gc-leaf-function"call-site attributes, and acompile_ll_to_objectpipe throughopt -passes='default<O2>,rewrite-statepoints-for-gc'gated onPERRY_RS4GC=1(loud failure withoutopt). Toolchain must be version-matched: LLVM 22optoutput is unparseable by Apple clang 21 (nocreateundeforpoison), soPERRY_LLVM_CLANGmust point at the same LLVM's clang — confirming this issue's "treat upgrades as an ABI migration" requirement concretely.Measured status with the surgery genuinely engaged: 5/8 gc-ratchet probes pass under
PERRY_GC_FORCE_EVACUATE=1 PERRY_GC_VERIFY_EVACUATION=1, byte-matched to the pinned Node oracle. Probes 01/06/08 fail — the first concrete reproducers of the double-typed dataflow frontier this issue predicted ("expose live heap references as managed ptr SSA … separate pointer payload from scalar/tag"): NaN-box values crossing statepoints asdouble/i64derivatives that RS4GC does not track. Metadata is not yet competitive either (probe 01: 6,992 B vs the explicit bridge's 5,320 B post-audit) — liveness pruning does not yet beat the audited elisions.Traps recorded for whoever continues: the first "all green" run was the fail-closed fallback silently carrying correctness while the surgery bailed on every function (the recognizer only knew the unit-test
alloca i64idiom; real roots arealloca double) — caught only by comparing record counts (200 vs the bridge's 55), not by any probe. Assert surgery liveness (grep for the cast signatures or compare record counts) before believing any RS4GC A/B.proggeramlug commentedon Aug 1, 2026
Second increment, same session (
16986ef85): the slice is now fully gated — 16/16 (8 probes × forced evacuation + verification, 8 × walker-verify), after two findings that are now design rules in the doc:default<O2>before RS4GC fails 3/8 probes: GVN/CSE merges the per-siteptrtoint/bitcastchains across future statepoint sites, recreating the stale-double hazard insideopt.function(mem2reg)alone is the sound pre-pass; clang's full optimization after statepoint insertion is safe by construction. (The branch's design law stated positively: relocation semantics must exist before the optimizer may move heap-derived values.)"gc-leaf-function"live) vs the audited explicit bridge's 55 on probe 01.The remaining ~48-record delta is RS4GC's zero-live statepoints — calls with no live gc values still get a 52 B record (header + 3 constant locations) that the bridge's per-call liveness skips outright. Post-compile record-stripping was analyzed and is a dead end for file size: dropped records can preserve function-table relocations (record arrays sit after the reloc'd table, and instruction offsets are function-relative), but shrinking a Mach-O section means a full object rewrite, and zero-padding keeps the disk bytes — parse-time win only. The honest remaining options are upstream (teach the pass to omit empty-live records — an LLVM patch), or accept the constant and win the axis via liveness quality on real code, where the bridge's conservative CFG-union retains roots RS4GC's SSA liveness drops.
Current metadata standing on probe 01: bridge 5,320 B / RS4GC 8,072 B. Next measurement that decides direction: the real-app (
test-drizzle-pg) comparison, where SSA-liveness pruning has far more dead-root mass to remove than the probes offer.proggeramlug commentedon Aug 1, 2026
Real-app measurement (
abaf2aa6a), and it flips the challenger hierarchy: ontest-drizzle-pg(133 modules), the RS4GC arm measures__text__llvm_stackmapsRS4GC generates the leanest text of any arm — 248 KB below shadow, 98 KB below the hand emission — and its metadata lands within 3.1% of the fully-audited bridge despite the zero-live-record constant and mem2reg-only liveness: SSA pruning compensates at scale exactly as this issue predicted. Smallest total native binary, 16/16 gate matrix, structural relocation model.
Direction for this issue is therefore settled: RS4GC is the preferred native backend, and the explicit bridge becomes a deletion candidate (a large one — its whole per-call rewriter and conservative CFG-union liveness) once RS4GC grows
has_trycoverage and either an upstream fix for empty-live records or acceptance of their constant. Remaining before any default-flip conversation is unchanged from the top of this issue: the ~25× gap versus shadow's zero metadata still routes through repsel promotion — but the native side of the ledger now has its winner.proggeramlug commentedon Aug 1, 2026
Runtime datum completing the slice's characterization: 8 probes × 9 interleaved reps (loaded host, arms share load — directional):
Consistent with the quiet-host mini matrix's finding that the statepoint family runs ~1% ahead of shadow, now with RS4GC's leaner codegen on top. Slice standing at session end: 16/16 gate matrix, leanest
__textof any arm (−248 KB vs shadow on drizzle), smallest total native binary, fastest directional runtime. Open: RS4GC-arm RSS (expected flat like the family, unmeasured),has_trycoverage, empty-live-record cost (upstream), and the quiet-host re-run of these runtime numbers.proggeramlug commentedon Aug 1, 2026
RSS cells completing the slice's table: RS4GC max-RSS is flat with shadow on the four churn-heaviest probes (27/27, 36/36, 37/38, 25/25 MB) — the family's RSS-neutrality holds for the RS4GC arm. Every axis of every configuration on the branch is now measured; the characterization table in
docs/statepoint-gc-experiment.mdhas no empty cells.proggeramlug commentedon Aug 1, 2026
Projection correction, measured: the shared assumption that landed repsel promotions erase native-root metadata has a measured slope of zero — batch.ts under statepoints is byte-identical with all promotion knobs off (24,752 B / 198 records / 33 slots, #7133-scoped knobs). Landed classes remove calls, not roots: they promote values the rooter's type analysis already classified non-pointer. Metadata erasure comes only from maybe-pointer-population promotions (untyped locals, temporaries, dependency JS — repsel Tracks E/F territory), which materially re-costs the '25× closes via repsel' path and strengthens the case that this issue's RS4GC liveness work and the repsel typed-slot story must land together. Details in docs/statepoint-gc-experiment.md on the branch.
fix(gc): mark inline asm as gc-leaf-function under RS4GC (#7174)
fix(gc): RS4GC leaf-marks inline asm even in rootless functions (#7174)
research(gc): first RS4GC pipeline slice (PERRY_RS4GC, #7174) - 5/8 p…
fix(gc): mark inline asm as gc-leaf-function under RS4GC (#7174)
fix(gc): RS4GC leaf-marks inline asm even in rootless functions (#7174)
proggeramlug commentedon Aug 3, 2026
Now part of one consolidated GC plan — see
docs/src/internals/rfc-rooting-by-construction.md("Where this sits in the one GC correctness plan", landed in #7292). Read that before starting work here, so five documents stop describing parts of the same shape.What changed for this issue: it is layer 2 of three, and it has a dependency. #7108 concluded that "the text-IR-plus-stock-clang architecture is what rules the cheapest design out" — Perry emits textual
.llto a user-suppliedclangand controls neither the pass pipeline nor stackmap emission. #7241 (in-process LLVM) removes exactly that constraint, and independently verified thatgc "statepoint-example"constructs, verifies and emits.⇒ Sequencing is 0 (#7241) → 2 (this). Starting statepoint migration before the in-process backend means fighting the architecture #7108 already measured as the blocker.
The first thing this layer must prove, before any migration commitment: that safepoint density is a usable lever on metadata size. The measured trade is 438,848 B of hot text saved against 4.5–16.6 MB of cold
.llvm_stackmaps. The model is24 B × (safepoint, root) pairsover 62,731 candidate safepoints, so filtering to genuinely-allocating calls should shrink it proportionally — but that is expected, not measured, and minimal binary size is a stated project goal.exp: pin layer-2 readiness — RS4GC schedules in-process
proggeramlug commentedon Aug 3, 2026
Cross-link: #7302 (try/catch → invoke/landingpad, deleting the setjmp path) is the prerequisite for removing this experiment's unsound
has_tryplain-stack-map fallback — with explicit unwind edges, statepoints can cover every function and relocations exist on both the normal and unwind paths. Phase 0 spike results and the cross-Rust-frame unwinding matrix are indocs/invoke-eh-experiment.mdonexp/invoke-eh.exp: pin layer-2 readiness — RS4GC schedules in-process
In-process LLVM backend: native construction via the C API, opt-in (#…
proggeramlug commentedon Aug 3, 2026
#7305 landed — the unsound
has_tryfallback can now be deleted.exp/stackmap-viabilitycurrently excludeshas_tryfunctions and routes them to a plain-stack-map fallback that is known-unsound, because alongjmpcan jump past agc.statepoint'sgc.relocatewrite-back.That is fixed at the source. #7305 replaced setjmp/longjmp with LLVM
invoke/landingpad(SEH funclets on windows-msvc) and deleted the setjmp path wholesale —volatile_setjmp.rs(376 lines),setjmp_abi.rs(193), thereturns_twice/noinlineattribute groups, the try-region volatile store tracking, and thePERRY_EHdevelopment flag. There is one exception lowering now, not two.With explicit unwind edges, relocations exist on both paths and no jump can skip one. So the exclusion and the fallback are both obsolete — and removing them closes the last soundness hole in the statepoint design rather than merely narrowing it.
Two other things that changed underneath this work:
rs4gc_schedules_in_processprovingrewrite-statepoints-for-gcruns as a module pass at pinned LLVM 22, emits the statepoint, and relocates the liveaddrspace(1)pointer. The pass pipeline is ours now — which is what makes stack-map density a lever rather than a fixed cost.returns_twice+noinline; they now inline and optimize like anything else, so any pre-Exception lowering: setjmp/longjmp → LLVM invoke/landingpad (#7302) #7305 measurement of statepoint overhead on try-heavy code is stale.Sequencing and the layer model are in
docs/engine-plan.md; tracker is #7294.Merge origin/main: invoke-EH lands, so statepoints cover every function
Native-frame GC roots via LLVM statepoints, opt-in (#7173, #7174) (#7314
gc: a stack-map record must belong to the function the ip is in (#7314 …
fix(codegen): module-unique names for promoted string constants and t…