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 closure in each independently retained lane;
  • no more than 1% control regression on call0_plain and call_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:

armclosure Machinstructions
legacy57.71 ms38,000,080
eager bind65.00 ms40,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/put operations 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_SETUP directly read the current closure’s immediate outer frame, store its PitValue in one new ordinary frame slot, and perform the first access in the same dispatch.
  • Later MACH_GETUP_CACHED / MACH_SETUP_CACHED read 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_cache is 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:

controlretaineddisableddelta
call0_plain44.31 ms44.74 ms-1.0%
call_hot14.79 ms15.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 PitValue slot).
  • 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