Archive — history, not state. Kept for its reasoning and its evidence; its plan is closed.
calling sequence v2 — measured cost attribution (before build)
Base: b1dbf1e6 (branch work/calling-v2), make check ALL GREEN. Machine
matches the honest chart in perf-next.md. All numbers median-of-7, fresh
daemon per fixture, PIT_ALLOW_NATIVE_LOAD=1, rows from observe.jsonl.
Reproduce: aot_bench/mt.ce (mach-only median) + aot_bench/attr/*.ce
(inlining-defeating call sweep), or add aot_bench/attr/* to vs.ce BENCHES.
Baseline (the four task rows)
| bench | mach ms | native ms | real calls | what it actually measures |
|---|---|---|---|---|
| fib | 75.76 | 27.89 | 2,692,539 | recursion; native uses the DIRECT self-call path |
| closure | 74.02 | 69.27 | 2,000,000 | dynamic call; native uses the TRAMPOLINE |
| call_hot | 16.06 | 7.95 | 2 | callee is INLINED away — not a call bench |
| record_field | 172.99 | 62.06 | 0 | generic record field RMW — a §3 records bench |
Census confirms the “real calls” column (aot_bench/census.ce):
- call_hot:
call 2— the streamline inliner collapses the known single target;add/mul/remainderare 2M each (the inlined body). call_hot measures arithmetic, not call overhead, in BOTH lanes. The plan’s “known-callee native calls already good (call_hot 7.5ms)” is because the call is gone, not because the call sequence is fast. - record_field: 0 calls, 126M ops dominated by is_record/is_stone/is_num guards + load/store. Out of scope for calling-v2; pure no-regression gate.
- fib: 2.69M
call+return+getup; closure: 2.00Mcall+return+setup. These two are the real call benches.
Attribution sweep (2M iters; dynamic dispatch via LCG+array defeats inlining)
floor = loop + LCG + dynamic array read + add, NO call. Each callN swaps
the array read for a dynamic call to one of 8 closures taking N args.
bigframe = call1 whose callee declares 24 extra locals (large nr_slots).
| fixture | mach ms | native ms | mach−floor | native−floor |
|---|---|---|---|---|
| floor | 28.19 | 12.91 | — | — |
| call0 (0 args) | 64.66 | 67.50 | 36.5 (18.2 ns/call) | 54.6 (27.3 ns/call) |
| call1 (1 arg) | 77.54 | 95.06 | 49.4 | 82.2 |
| call2 (2 args) | 91.97 | 104.13 | 63.8 | 91.2 |
| call4 (4 args) | 124.55 | 104.92 | 96.4 | 92.0 |
| bigframe (24 locals) | 112.74 | 84.41 | 84.6 | 71.5 |
(mach-only mt.ce agrees: floor 27.45, call0 61.7, call1 79.37, call2 91.95, call4 121.72, bigframe 114.09.)
What the numbers say (design against THIS, not assumptions)
The native trampoline is the single biggest structural cost. A 0-arg dynamic call+return through the trampoline is 27.3 ns — SLOWER than the same call on mach (18.2 ns). That is exactly why closure-native (69.3) ≈ closure-mach (74.0). The trampoline =
pit_rt_signal_call+ret PIT_NULLtopit_native_dispatch+ loop re-entry + frame re-derivation + re-validate fn/arity + re-enter. fib-native avoids all of it (direct self-call path) and is 2.7× faster than fib-mach. Killing the trampoline for dynamic native callees is the highest-value change (element 5).The mach dynamic call+return floor is ~18 ns, ~4 ns of which is the trivial callee body (getup+return); ~14 ns is pure machinery: frame alloc + size negotiation + is_func + arity + dispatch + return plumbing. The overlapping-frames/precomputed-size/redundant-work reductions target this ~14 ns.
Frame zero-init is LOW value. bigframe adds 24 slots but census shows it also adds ~14 real ops/call (the 24 assignments+guards); netting those out, per-slot frame init is < ~0.2 ns/slot — a few ns/call even for a big frame. The watermark-init element (4) is not where mach time goes, and it couples to the GC scanner (risk). Deprioritize / treat as optional.
Arg copy is LOW value on its own. The per-arg deltas (call0→call4) are dominated by callee-side arg use (an add + is_num guard per arg), not the copy. Overlapping frames’ payoff is contiguous frame reuse + removing the separate copy loop’s bookkeeping, not the loads/stores.
call_hot is not a call benchmark (inlined). Do not use it to judge call-sequence changes; use fib (mach) and closure (both lanes). Keep it as a no-regression row only.
Priority order chosen (measured value × safety)
- P1 native: replace the dynamic-call trampoline with an inline call (no
signal_call/resume round-trip). Target: closure 69 → ≤25 ms. Constraint: C-stack depth stays bounded byctx->stack_limitexactly as the existing direct-safe/hosted path already is. - P2 mach: remove redundant work in the hot MACH_CALL register path (dead pre-alloc of the packed frame-info int that the register path overwrites; the double fn/is_function derivation) + precomputed frame size. Target: fib 76 → ≤40 ms. Safe, enumerable, no GC-invariant change.
- Deprioritized: watermark frame-init (low value, GC-coupled risk), arg-copy elimination in isolation. Kept as open questions.
OUTCOME (built, gated)
P1 landed: non-direct-safe dynamic native calls lower to an inline call
(pit_rt_call_dynamic); a callee proven stack-safe at compile time (creates
no closure, never signals the dispatcher) gets a LIFO frame-stack frame and a
direct call instead of a heap frame + dispatcher. See the calling-v2 +
reseed commits.
| bench (native) | before | after | note |
|---|---|---|---|
| closure | 69.27 | 33.07 | 2.1×; frame alloc 152 MB → ~0; match=Y |
| fib | 27.89 | 28.2 | unchanged — already on the direct-safe path |
| call_hot | 7.95 | 7.0 | unchanged — inlined |
| record_field | 62.06 | 62.5 | unchanged — no calls |
mach lane unchanged (native-emitter-only). Gates: make check ALL GREEN
(vm suite / language suite / fuzzer); full vs.ce matrix 32/32 match=Y (native_call
excluded — its math/radians C module is unbuilt in this fresh shop, a mach-lane
env gap orthogonal to calling-v2); frame-GC regression fixtures under natural
heavy GC all match=Y (aot_bench/stress/, via aot_bench/nstress.ce).
Not reached, and why: closure ≤25 ms needs closure-body inlining (a separate compiler arc; the calling-sequence win alone is the 33 ms above). fib-mach ≤40 ms needs the mach overlapping-frames restructure — deferred (criteria #3: mach lowering is a later pass; the measured mach cost is fixed per-call machinery, and a safe incremental tweak cannot reach a 46% cut).
Source: plans/archive/perf-2026-07/calling-v2-measurements.md