Repository navigation
gc: function-entry polls at indirect entries and recursive SCCs (RFC deferred collection S5, polls) - #11631
gc: function-entry polls at indirect entries and recursive SCCs (RFC deferred collection S5, polls)#11631proggeramlug wants to merge 30 commits into
Conversation
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueThanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
3b34d3e to
978d7c3
Compare
978d7c3 to
4c83a6f
Compare
30d37e6 to
cd9aae1
Compare
…; no spec-ABI name in docs
…ecker counts pump calls and merges back edges
cd9aae1 to
593a229
Compare
| if let Some(rep) = rep { | ||
| scc_reps.insert(rep.clone()); | ||
| for name in &scc { | ||
| polled.insert(name.clone()); |
There was a problem hiding this comment.
[P2] Cover allocating subcycles before marking the whole SCC polled
One selected member does not intersect every cycle in an SCC. For example, with direct edges a -> {a, b} and b -> {a, b}, and an allocation in each function, the tie-break selects a and removes b's scaffold. A branch repeatedly taking b -> b can then allocate without reaching an entry poll, even though both functions are inserted into polled. This leaves that path dependent on the allocation valve rather than providing the intended poll bound.
I reproduced the matching checker blind spot using this head's scripts: LLVM IR with that graph, a poll only in a, and a direct js_object_alloc call in each function exits 0 with --max-uncovered-sccs 0 (allocating=1 uncovered=0); removing the sole poll correctly exits 1. This is a source/checker reproduction, not a compiled runtime measurement.
Please retain enough polls to intersect every allocating recursive cycle (for example, recheck the remaining graph after cutting at polled entries), and add a subcycle negative control to the coverage checker. The same check is needed when an existing indirect-entry poll causes the whole SCC to be marked covered.
RFC deferred collection (#11528), step S5, the polls (decision 2). Stacked on #11630 (
gc/s5-runtime-invariant): review that first. The diff here is the last commits.What it adds
__perry_wrap_*forwarders that make a top-level function a callback value.How a poll is placed.
argumentsmaterialisation (which can allocate). The scaffold is the armed-wordload volatile, a branch, andcall void @js_gc_entry_safepoint(). It is the back-edge poll under another symbol: same runtime body, same seeded-schedule candidacy, samePERRY_GC_SCHEDULE_ALLOC_KBpacing.entry_polls::finalize_modulekeeps a scaffold only in a function that the module leaf analysis, with entry polls ignored, already proves collecting. So every call to it was already a statepoint, and no call changes classification (S5 must not; S6 does that). A dropped scaffold's load becomes a constant and LLVM folds the branch away.js_gc_entry_safepoint_argsroots them across the collection and writes the relocated values back.Kill switch. It is the loop polls' own,
PERRY_GC_MOVING_LOOP_POLLS=0. There is no new knob.S1 tables.
js_gc_entry_safepointandjs_gc_entry_safepoint_argsareReentersin all threegc_effects/*.tsvfiles, and declaredpollseeds inseeds.txt. The wasm ABI table is regenerated.Proposed refinement of decision 2, with evidence
Decision 2 says "one poll per recursive SCC". Implemented literally, that put a poll in
benchmarks/suite/05_fibonacci:fib's numeric fast clone and its boxed fallback form an SCC. The run cost +59.98% instructions, a poll on every call of a recursion that allocates nothing.This PR therefore polls a recursive SCC only when the recursion itself allocates without a poll: some member has a collecting edge of its own, or calls out of the SCC to a function that may collect and does not poll at its entry. SCCs are visited callees first, so a callee's poll is known in time.
Result:
05_fibonaccirectree(loop-free recursive allocator)The rule is stated as a proposal on #11528. The checker (
scripts/gc_poll_coverage_check.py) applies the same definition, so a violation of either reading shows up there.Poll-coverage checker
scripts/gc_poll_coverage_check.py, run over emitted IR:--max-uncovered-loops/--max-uncovered-sccsturn the counts into a ratchet.--self-testproves it can fail.It also answers the §2 open item: the specialized
for/for-of/for-inlowerings named in thestmt/loops.rscomment are now measured rather than assumed.Measurements
qb4, same base, same 32 programs as #11630, instructions as medians of 5.
Compute and RSS.
05_fibonaccirectree(loop-free recursive allocator)Poll counts and cost. From
--statepoint-report=text, which now prints anentry polls:line:09_method_calls14_closure05_fibonaccirectreecc bundle (claude-code 2.1.112):
--versioninstructions--helpinstructions--versionpeak RSS--helppeak RSSCompile wall (67 min) and compile peak RSS (37.8 GB) are equal to main. The cc poll count is not reported: that compile predates the report line.
Decision 3 (straight-line bodies).
max_poll_wait_bytesis 0 on:The valve never fired on any of them, so no statement-boundary polls were added. The cc bundle's init chunks under a real session remain unmeasured until #11559 lets cc run
-pturns.Tests
crates/perry/tests/gc_entry_polls_s5.rs(real TS → IR):entry_polls.rsunit tests: Tarjan on cycles and self-loops, a 50k-deep chain without recursion, callees-first order.gc_poll_coverage_check.py --self-test.perry-codegen: twoshadow_inlinetests pin exact register numbers; their pins moved by the scaffold's two registers.