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:
| Row | Mach | Native | Allocation / GC |
|---|---|---|---|
| no-call floor | 25.0-25.7 ms | 17.0 ms | about 1 KiB / 0 |
call0_plain | 45.1-46.0 ms | 49.1 ms | about 2 KiB / 0 |
| captured call0 | 53.9-54.2 ms | incorrect in native at that checkpoint | about 2 KiB / 0 |
call_hot, inlining disabled | 31.4 ms | 28.1 ms | about 1 KiB / 0 |
| closure control | 59.2 ms | 31.0 ms | no GC |
| Fibonacci control | 54.9 ms | 29.1 ms | no 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 arm | Mach | Change | Memory behavior |
|---|---|---|---|
| eager stale-slot scrub | 40.46 ms | +9.09 ms | no retention, but slots were cleared twice |
| scanned stale-slot retention | 36.74 ms | +5.37 ms | retained 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