Repository navigation
gc: reduce safepoint density (safepoint polling instead of every-call statepoints) #8596
Description
Activity
- addedperformanceRuntime, compile-time, build-size, or memory performanceRuntime, compile-time, build-size, or memory performance
on Aug 22, 2026 Safe increment filed: #8598
Four provably-non-collecting helpers reclassified as
gc-leaf-function(verified against their runtime bodies + already in theNONCOLLECTINGauthority):js_typed_feedback_closure_direct_call_guard,js_implicit_this_get,js_tdz_suppress_begin/_end. Zero semantic risk, trims safepoints across all functions. Not the fan-out fix — that's the polling model below.Design notes — the real lever (safepoint polling)
The
× safepointsterm shrinks materially only by moving from "every non-leaf call is a safepoint" to safepoint polling: collection may begin only at a bounded set of poll points; ordinary calls stop being safepoints, so RS4GC statepoints (and relocates) collapse to the poll/alloc sites.- Pieces perry already has:
GC_SAFEPOINT_PENDING+js_gc_loop_safepoint(the loop back-edge poll), andgc/pressure.rsalready defers a pressure-triggered collection to the next poll when a generated frame is live. Today that coexists with statepoints-at-every-call; polling would make the poll points the only safepoints. - What it touches: (1) codegen — emit poll checks at loop back-edges, allocation sites, and returns, and stop treating calls as safepoints in
function/precise_roots.rs; (2) the moving scavenge (gc: default-on evacuating young-gen scavenge (moving minor at precise safepoints) #7019) — a moving collection may run only where all live frames' roots are precisely known, i.e. a poll (a non-moving one can be conservative anywhere); (3) the conservative-scan fallback for a thread caught between polls; (4) the write-barrier /AllocNoReentry"alloc-point valve" contract, which already assumes deferral-to-safepoint semantics; (5) invoke/unwind rooting (codegen: lower try/catch to LLVM invoke/landingpad, delete the setjmp/longjmp path #7302). - Why it's separate + large: it changes collection-latency and progress guarantees (every loop back-edge must poll; no unbounded poll-free call chain), so it needs a flagged prototype validated against the gc-stress suite before it can be a default. This is where most of gc: reduce safepoint density (safepoint polling instead of every-call statepoints) #8596's value — and its risk — lives.
Bonus finding (checker over-inclusion — not a live bug)
While auditing, two helpers listed in
scripts/gc_root_dominance_check.py'sNONCOLLECTINGset actually allocate; the Rustclassify_direct_calleecorrectly leaves bothUnknown, so there is no miscompile, but the checker could pass a build with stale roots across such a call:js_closure_unbox_callee_checked(closure/unbox.rs:25) — non-callable path isthrow_not_callable()→js_throw_type_error_not_a_function, which allocates the TypeError and unwinds.js_class_object_pin_parent(object/class_registry/parent_static.rs:289) — callsjs_string_from_bytesandjs_object_set_field_by_name; both allocate.
Worth tightening the checker's set (or documenting why they're admitted there).
js_object_mark_classis undetermined — it callstransition_object_shape_to_class, which needs a shape-descriptor-allocation audit.- Pieces perry already has:
- added a commit that references this issue
on Aug 22, 2026
Summary
Perry treats every call as a GC safepoint:
rewrite-statepoints-for-gcwraps each non-leaf call in agc.statepointand relocates every live root across it. That is correct (any callee might collect) but it makes the safepoint count of a function equal to its call count, which is the× safepointsterm in the RS4GC relocation fan-outlive_roots × safepointsbehind #8583 (the Claude Code@mainhas ~106k call sites).Many production GC'd VMs instead use safepoint polling: collection can only begin at a bounded set of poll points — loop back-edges, function returns/entries, and allocation sites — and ordinary calls are not safepoints unless the callee itself polls. This shrinks the safepoint set dramatically without losing the guarantee that a collection can always make progress, because a thread reaches a poll in bounded time.
Proposal (exploratory — needs design)
Move from "every call is a safepoint" toward a polling model:
gc-leaf-style edge that does not relocate roots;GC_SAFEPOINT_PENDING,js_gc_loop_safepoint, the loop-poll path).Why it's worth it
Unlike #8595 (outlining), which fixes the giant-function symptom, reducing safepoint density shrinks the
× safepointsfactor for every function in the codebase, not just the outlier — lowering RS4GC cost and stack-map size across the board, and reducing the number of points at which roots must be kept live (better register allocation).Scope / risk
This is the largest of the three and a genuine GC-architecture change: it affects collection latency/progress guarantees, the interaction with the moving scavenge (#7019), write barriers, and the conservative-scan fallback. It should be scoped and prototyped carefully — filed here so the option is tracked rather than lost, not as ready work.
Context: surfaced while fixing #8583 (RS4GC relocation fan-out). Complements #8595 (outlining) and the root-spilling stopgap in #8583. Root-lowering background: #7370 (native roots default), #7280/#7173/#7174.