Archive — history, not state. Kept for its reasoning and its evidence; its plan is closed.
Closure outer-frame fast-path experiment
Date: 2026-07-14. Campaign baseline: compiler_optimizing at the shared
campaign checkpoint. This is a target-private lowering experiment: semantic
mcode and language/runtime semantics are unchanged.
Status: retained Mach optimization; final campaign integration is green.
Question
The honest dynamic closure microbenchmark invokes one of eight distinct
capturing closures through one unpredictable call site two million times. Each
closure performs a depth-1 captured read, increments it, writes it back, and
returns it. Before this experiment every Mach get and put independently
called mach_resolve_outer_frame, repeating current-function validation and
the lexical-link walk. Native QBE similarly regenerated its pointer chain for
each access.
The gate set before implementation was:
- at least 5% faster on
closurein each independently retained lane; - no more than 1% control regression on
call0_plainandcall_hot; - at most one hidden Mach slot (+8 bytes per live eligible activation);
- no semantic mcode opcode, no weakening of copying-GC barriers, stack freeze, suspension, disruption, or depth-2 closure behavior.
Rejected trials
Eager Mach prologue bind
The first implementation emitted a private BIND_OUTER prologue followed by
cached get/put operations. It was correct but added one VM dispatch per call:
| arm | closure Mach | instructions |
|---|---|---|
| legacy | 57.71 ms | 38,000,080 |
| eager bind | 65.00 ms | 40,000,080 |
The cache bookkeeping cost more than the resolver walk it removed. This form was deleted.
Native block/dominance-local outer-base CSE
QBE reused the immediate-parent frame base across a pure get/add/put region. The canonical numeric disruption diamond required a narrowly proven carry over one unique-predecessor success edge; labels, calls, allocation, GC, suspension, disruption, and ambiguous joins all invalidated it. It used no runtime memory and preserved the captured-put write barrier.
The structural test proved that it removed one native pointer-chain walk, but runtime results were only 26.60-27.09 ms versus 26.96-27.19 ms legacy. This was noise-sized (well below the 5% gate), so all native CSE code, metadata, flag, and tests were deleted. Native closure access remains unchanged.
Retained Mach arm
The winning form combines the first dominating depth-1 access with binding:
- Eligibility requires at least two depth-1
get/putoperations and requires the first semantic instruction to be that first access. This makes the bind dominate every later access without retaining a CFG or adding metadata. MACH_BIND_GETUP/MACH_BIND_SETUPdirectly read the current closure’s immediate outer frame, store itsPitValuein one new ordinary frame slot, and perform the first access in the same dispatch.- Later
MACH_GETUP_CACHED/MACH_SETUP_CACHEDread that scanned slot. - The slot is inside the normal frame range, so copying GC rewrites it and frozen/suspended stacks already preserve it. There is no C side table or raw unscanned pointer.
- Captured writes retain
pit_gc_write_barrier. Depth 2 and deeper use the existing generic resolver. - Lowering conservatively pre-counts later key and tolerance scratch. It declines the cache rather than making a previously valid 8-bit-slot function overflow 255 slots.
PIT_STREAMLINE_DISABLE=mach_outer_cacheis the exact A/B lever. Only the disabled arm carries_mach_outer_cache=false; default semantic instruction arrays remain unchanged.
The fused form adds no dispatch and no serialized bytecode bytes. It changes the first and later access opcode numbers in-place. Three alternating retained samples were 53.64, 52.64, and 54.56 ms (median 53.64). Three legacy samples were 57.71, 58.86, and 61.06 ms (median 58.86): 8.9% faster. Both have exactly 38,000,080 counted Mach instructions and the same 2 KiB measured allocation with zero GC.
Controls contain no eligible capturing function and serialize the same Mach instruction streams. Later adjacent samples were:
| control | retained | disabled | delta |
|---|---|---|---|
call0_plain | 44.31 ms | 44.74 ms | -1.0% |
call_hot | 14.79 ms | 15.51 ms | -4.6% |
Earlier alternating control samples varied in both directions while opcode counts stayed identical; the adjacent pair above is the clean control gate. Native timings and IL are unchanged by the retained Mach-only arm.
Memory and code tradeoff
- Eligible live Mach activation: +8 bytes (one
PitValueslot). - Eligible serialized function: 0 bytes growth; a regression test compares exact blob lengths with the pass enabled and disabled.
- Ineligible function: 0 live bytes and no bytecode change.
- Runtime text: four small private handlers plus names/dump cases. There is no per-function side table and no persistent compiler fact.
Verification
make: C runtime rebuild passed.tests/closure_outer_codegen.cm: direct cached execution, exact serialized size, and 254-slot-plus-key-scratch fail-closed coverage.tests/closure_outer_runtime.ce: distinct closures sharing code, captured mutation, depth-2 nesting, recursive activation, disruption, later-turn stack freeze, copying-GC movement/promotion, pointer write barrier.tests/compile.cm: 138/138.tests/mach_suite.ce: 57/57.
The campaign integration gate subsequently passed make seed, 7/7 Meson
tests, 145/145 focused compiler tests, 1,086/1,086 isolated VM checks,
1,921/1,921 warmed full-suite checks, and 3,733/3,733 deterministic fuzz checks
over 500 programs. The final clean-daemon canary measured closure at
51.42 ms Mach / 25.97 ms native with the cache and 56.18 / 26.24 ms disabled:
an 8.5% Mach win, with the native difference noise-sized as expected. The
final aggregate row was 50.88 / 26.24 ms. The retained cost remains exactly
one scanned 8-byte slot per live eligible Mach activation and zero serialized
Mach-byte growth.
Source: plans/archive/perf-2026-07/perf-closure-outer-cache.md