Summary
asyncpipe runs zero GC cycles — no [gc] line is ever printed — and yet ~33% of its leaf profile is collector machinery the mutator pays anyway: write barriers, per-object layout side tables, and RuntimeHandleScope rooting.
PERRY_GC_MOVING_LOOP_POLLS=0 measures −14.1% on asyncpipe while still running zero GC cycles. So the cost is not collection — it is an incremental old-gen mark/sweep being driven at back-edge polls that never completes a cycle and never reports one.
This is the largest single lever found in the asyncpipe round and it was deliberately left open: turning polls off is not a shippable fix (polls are default-ON since #7721 and removing them has its own history — see #7690, and the note in CLAUDE.md that a poll in a call-free clone deletes the element-shape clone). The question is why a program that never collects pays 14% for incremental collection.
Why this matters beyond one benchmark
It falsifies a premise this campaign has been using: "zero GC cycles" was treated as "GC levers are irrelevant here." That is wrong. A pure-mutator program still pays incremental-collector machinery, and on asyncpipe that is the top item.
PERRY_WRITE_BARRIERS=0 is +0.9% here, so codegen write barriers are genuinely not the cost — the incremental drive is.
Suggested direction
- Establish why incremental old-gen work is being scheduled at all on a heap that never triggers a collection. If the answer is "the poll unconditionally does a slice of work", the fix is a cheap due-check before the slice, not disabling polls.
- Any fix must assert its subject was live (per CLAUDE.md's four-ways-a-gate-cannot-fail): a gate here must show incremental slices actually ran on the baseline, or it proves nothing.
Evidence
gc-handoff/ASYNC2-NOTES.md — decomposition, counters, and reproduce commands. Measured with instructions retired (dev box was at load 12–165; see gc-handoff/MEASUREMENT-PROTOCOL.md).
Summary
asyncpiperuns zero GC cycles — no[gc]line is ever printed — and yet ~33% of its leaf profile is collector machinery the mutator pays anyway: write barriers, per-object layout side tables, andRuntimeHandleScoperooting.PERRY_GC_MOVING_LOOP_POLLS=0measures −14.1% onasyncpipewhile still running zero GC cycles. So the cost is not collection — it is an incremental old-gen mark/sweep being driven at back-edge polls that never completes a cycle and never reports one.This is the largest single lever found in the
asyncpiperound and it was deliberately left open: turning polls off is not a shippable fix (polls are default-ON since #7721 and removing them has its own history — see #7690, and the note in CLAUDE.md that a poll in a call-free clone deletes the element-shape clone). The question is why a program that never collects pays 14% for incremental collection.Why this matters beyond one benchmark
It falsifies a premise this campaign has been using: "zero GC cycles" was treated as "GC levers are irrelevant here." That is wrong. A pure-mutator program still pays incremental-collector machinery, and on
asyncpipethat is the top item.PERRY_WRITE_BARRIERS=0is +0.9% here, so codegen write barriers are genuinely not the cost — the incremental drive is.Suggested direction
Evidence
gc-handoff/ASYNC2-NOTES.md— decomposition, counters, and reproduce commands. Measured with instructions retired (dev box was at load 12–165; seegc-handoff/MEASUREMENT-PROTOCOL.md).