Archive — history, not state. Kept for its reasoning and its evidence; its plan is closed.
Native fannkuch loop-pressure experiment
Date: 2026-07-13
Baseline: 7fca24a4 (the exact parent performance-campaign tip when this work
started). The branch also contains the independently validated typed CLI reply
deadline change as 70c4e0b1; it does not affect generated language code.
Result
Keep one narrow native lowering improvement. When QBE’s existing structured loop proof finds at most six loop-carried values, behavior is unchanged. When a function has more than six candidates, it no longer discards every opportunity: it keeps at most six values that are both:
- exact
intaccording to the final point-specific compiler facts, and - proven
_int_bounded, so the value remains an int32 lane.
Raw doubles and tagged booleans are deliberately not retained in this overflow case. There is no slot-number, benchmark-name, source-name, target-name, or PGO special case. No semantic mcode, runtime ABI, frame layout, GC layout, or native suspension convention changed.
On fannkuch, the final optimized function has eight candidate physical slots:
| Slot | Lane |
|---|---|
| 5 | bounded raw int |
| 8, 9, 10, 11, 13, 18 | raw double |
| 19 | tagged bool |
The accepted rule selects only slot 5. Across nine fresh-process, median-of-11
native measurements, the control aggregate median was 107.330 ms and the
candidate aggregate median was 106.135 ms: a 1.11% improvement. The candidate
won 8 of 9 like-for-like directional pairs. The best paired observation was
105.972 -> 103.096 ms (-2.71%), but the defensible claim is about 1%, not 2.7%.
Every run returned the exact expected value 3000008629.
Generated code also shrank:
| Artifact | Control | Candidate | Change |
|---|---|---|---|
| QBE IL bytes | 167,625 | 167,229 | -396 |
| assembly bytes | 114,287 | 114,043 | -244 |
Why the broader approaches were rejected
The opt-in diagnostic constructs final-index natural loops and reports candidate types, loop nesting, loop-membership overlap, and an actual backwards CFG liveness fixed point. Fannkuch’s eight-candidate union has a peak CFG liveness of only six, which initially looked as though all eight candidates might fit. That inference was wrong at the backend boundary.
Selecting all eight reduced QBE IL but substantially regressed native code:
| Policy | Native median | Versus 105.972 ms control | IL bytes | Assembly bytes |
|---|---|---|---|---|
| Reject all eight (control) | 105.972 ms | - | 167,625 | 114,287 |
| Select all eight | 127.451 ms | +20.3% | 161,653 | 121,943 |
| Select bounded raw int only | 103.096 ms | -2.7% in this pair | 167,229 | 114,043 |
The all-eight arm made IL 3.6% smaller but assembly 6.7% larger. Thus physical slot liveness is not enough to predict QBE/host register pressure, conversion traffic, and spill cost.
Every one-candidate extension on top of the integer was also slower and was deleted:
| Added carried value | Native median |
|---|---|
| tagged bool slot 19 | 106.124 ms |
| raw double slot 11 | 107.316 ms |
| raw double slot 13 | 128.533 ms |
| raw double slot 18 | 110.473 ms |
| raw double slot 8 | 110.605 ms |
| raw double slot 9 | 110.182 ms |
| raw double slot 10 | 115.656 ms |
This is why the final rule uses a semantic lane class rather than a pressure
heuristic or a particular physical slot. All experiment selectors were removed.
The expensive liveness fixed point and detailed records are strictly gated by
_loop_phi_diagnostics; normal compilation does not execute them and does not
use them to select code.
Benchmark-corpus audit
aot_bench/dumpil.ce <file> loopphis was run over every source in
aot_bench/shootout, aot_bench/suite, and aot_bench/attr (43 sources).
Fannkuch was the only source whose candidate union exceeded six, so it is the
only existing benchmark whose generated code changes.
Notable unchanged selections:
- spectralnorm: two candidates in each hot helper and two in main;
- binarytrees: one candidate in main;
- array_read, closure, and the call-attribute fixtures: two candidates;
- record_field /
d_field: three candidates; - mandelbrot, nbody, call_hot, loop_nested, fib, and the integer-overflow suite fixtures: no eligible structured-loop candidates in this lowering mode.
Exact candidate/control artifact comparisons were byte-identical for unrelated performance gates:
| Source | QBE IL bytes | Assembly bytes |
|---|---|---|
| spectralnorm | 176,497 | 117,795 |
| loop_nested | 27,246 | 25,574 |
| mandelbrot | 63,619 | 43,227 |
| call_hot | 39,324 | 33,247 |
Timing differences on those byte-identical artifacts are noise and are not credited to this change.
Suspension, GC, and correctness
native_suspend_loop_phi_overflow.ce is a real-source oracle whose final IR has
nine carried candidates: eight raw doubles and one bounded raw integer. The
overflow rule selects only the integer. Both Mach and native return 2125000.
The native suspension runner, with a reduction budget of 64 and
PIT_SUSP_TEST_GC=1, returned the same value after 48 suspensions and 48 moving
collections. This exercises the required canonical tagged frame spill across
pause/resume; the actor stack remains freezable.
Validation commands and results:
./pit test tests/compile.cm
115 passed, 0 failed
./pit test
1918 passed, 0 failed
./pit fuzz 250 --seed 20260713
1891 differential checks, 0 failed
./pit fuzz 250 --seed 20260963
1842 differential checks, 0 failed
total fuzz: 3733 checks, 0 failed
PIT_INPROCESS=1 PERF_LOC=aot_bench/regress/native_suspend_loop_phi_overflow.ce \
./pit aot_bench/perf_one.ce run
Mach result 2125000; native result 2125000; match=true
PIT_SUSP_TEST_GC=1 native_suspension_test <dylib> 2125000 64
value=2125000; exception=0; suspensions=48; max_depth=1; gc=48
The fuzz campaign was split only because the current terminal-owned one-shot target is auto-reaped at 60 seconds. One 500-program command reached 475/500 and 3,545 passing checks at monotonic 58.961 seconds before losing its terminal target. The two 250-program commands use the same deterministic contiguous campaign convention already used by the performance suite, finish in about 31 seconds each, and together cover the required 3,733 checks.
Memory and code-size tradeoff
Target/runtime memory cost is zero:
- no additional Pit frame slots;
- no persistent side tables;
- no runtime heap objects;
- no code metadata;
- no altered GC roots or suspension records.
The accepted fannkuch output is 244 assembly bytes smaller. The diagnostic has
compiler-only temporary arrays/records and a backwards-liveness fixed point,
but it exists only when a caller explicitly sets _loop_phi_diagnostics.
Normal target generation pays only the small bounded-integer filtering loop
when the already-computed candidate union exceeds six.
Tooling changes and friction found
aot_bench/dumpil.ce was converted from environment-variable/bare-script use
to the signed-off typed request/reply CLI:
./pit aot_bench/dumpil.ce FILE raw|mcode|il|asm|loopphis
It now has an explicit 600-second reply deadline, returns success/failure, and
can print the opt-in loop diagnostic. aot_bench/perf_one.ce now advertises the
same 600-second reply deadline. These changes do not affect compiled programs.
The lifecycle investigation found a separate tooling defect: terminal-owned one-shot targets still receive the default 60-second auto-reap timer even when their typed help declares a longer reply timeout. Cold compiler realization can therefore continue in the shop/builder after the test or fuzz target and its terminal have been stopped. Warm reruns pass. The parent campaign separately captured the exact 60.0239-second reap and is fixing the terminal start contract; that lifecycle fix is intentionally not part of this compiler experiment.
Recommendation
Keep the bounded-raw-int overflow fallback. It is a small, target-independent compiler lowering improvement with no runtime memory cost, smaller code, exact suspension/GC coverage, and a repeatable ~1% fannkuch-native gain. Do not keep the all-candidate, raw-double, bool, or CFG-pressure selectors: all were measured and rejected.
Source: plans/archive/perf-2026-07/perf-native-fannkuch-loop-pressure.md