Archive — history, not state. Kept for its reasoning and its evidence; its plan is closed.

Dynamic call activation-cache experiment

Date: 2026-07-12

Original measurement branch: codex/call-activation-experiment at 70a0922f. The prototype was deleted. This record is copied into the contained performance branch so the failed approach is not rediscovered after the original worktree disappears.

Cost census

call0_plain performs two million dynamic zero-argument calls through an array of eight functions without captured environments. It separates dynamic call activation from closure outer-frame resolution.

Representative medians on the campaign machine were:

RowMachNativeAllocation / GC
no-call floor25.0-25.7 ms17.0 msabout 1 KiB / 0
call0_plain45.1-46.0 ms49.1 msabout 2 KiB / 0
captured call053.9-54.2 msincorrect in native at that checkpointabout 2 KiB / 0
call_hot, inlining disabled31.4 ms28.1 msabout 1 KiB / 0
closure control59.2 ms31.0 msno GC
Fibonacci control54.9 ms29.1 msno GC

The Mach floor executed 32,000,052 operations and call0_plain executed 40,000,061. The exact delta is 8,000,009 across two million calls: about four operations per call (call, callee loadi, return, caller move). At the floor dispatch rate those explain about 3.1 ns of the 10.1 ns/call total increment. Activation and return account for roughly 6.9 ns/call. Comparing the same call_hot work with inlining disabled gives an approximately 8.9 ns/call Mach residual and 10.5 ns/call native residual. Captured outer frame resolution adds approximately another 4.5 ns/call in Mach.

The ordinary Mach child frame is cheap: bump a LIFO frame, stamp function, caller, address, copy/null-fill slots, switch code/PC, and pop/reconstruct on return. A zero-argument one-slot child occupies 40 bytes; main plus child peak was about 232 bytes.

Rejected cache

The experiment reused a prepared child frame at statically resolved, exact-arity, nonescaping sites. Dynamic/apply calls, recursion, reentrancy, under/over-application, and escaped/promoted frames retained the general path. It added no semantic mcode operation; a temporary private Mach encoding used the existing CALL/CALLARGS representation.

Correctness exposed two costs before timing:

  • cached children have LIFO ownership and must be drained before their owner returns;
  • a nested call overwrites a frame’s address metadata, so reuse needs a validity tag and careful disruption cleanup.

Two GC-safe implementations were measured against 31.37 ms:

Cache armMachChangeMemory behavior
eager stale-slot scrub40.46 ms+9.09 msno retention, but slots were cleared twice
scanned stale-slot retention36.74 ms+5.37 msretained one scanned 40-byte child/site and stale locals until caller return

Both regressed. The existing bump/stamp/pop path is cheaper than cache lookup, ownership, clearing, scanning, and unwind bookkeeping. The implementation and encoding were deleted.

Do not retry frame reuse unless the frame representation or GC scanning contract changes materially. Knowing a callee through static analysis or PGO does not overturn this result.

What the result points toward

The remaining call lever is not activation-storage reuse. It is a thinner freeze-safe entry/continuation/return convention:

  • guard or prove the callee’s code identity while retaining the real closure environment;
  • preserve a GC-walkable, relocatable Pit continuation at every suspension boundary;
  • reduce metadata writes, pointer chasing, caller-code reconstruction, and tagged spill slots;
  • retain the general path for truly polymorphic, apply, disruption, and invalidated cases.

Later campaign work validated this direction only for a very narrow raw recursive Fibonacci convention: 104-byte private activations became 56 bytes and native time fell from 23.820 to 13.947 ms. It did not improve call_hot, so it is evidence for exact continuation representations, not a general call solution. See perf-native-compact-raw-continuation.md.

Separate correctness finding

The original branch also localized a native attr-call failure to QBE remainder lowering, not closures. Conservative destination type num caused an integer remainder to be encoded as a boxed float; the following is_int rejected it and selected null before the call. Raw and streamlined mcode were correct and Mach returned the expected value. Any recurrence of this fixture should be triaged at QBE numeric/remainder lowering rather than calling-convention code.

Source: plans/archive/perf-2026-07/perf-call-activation-cache.md