Archive — history, not state. Kept for its reasoning and its evidence; its plan is closed.
Mach guard/branch fusion experiment
Date: 2026-07-13
Base: 2601786e
Branch: codex/perf-mach-guard-fusion
This is a contained campaign experiment. It changes no semantic mcode and no language behavior. It rewrites selected adjacent Mach words after load, just like the existing compare/branch and field superinstructions.
Motivation and exact shape
After streamline’s guard elimination, record and array lowering still contains large numbers of irreducible pairs of this form:
is_record result, value
jump_false result, fallback
The type test writes an immediate boolean and cannot allocate or disrupt. The following branch consumes that exact boolean. The old VM paid two dispatches. The experiment rewrites only the first word to a Mach-internal fused opcode; the second word stays in the instruction array and is still independently jump-targetable. The fused handler:
- runs the identical predicate;
- writes the identical boolean result slot (it is not treated as dead);
- consumes the original branch word and applies its original relative offset;
- performs the same pause/interruption check on a taken backward branch.
Six measured-hot, pure predicates are covered: is_int, is_num, is_array,
is_func, is_record, and is_stone. PIT_MACH_NO_GUARD_FUSE=1 disables
only this family for exact A/B tests; the existing PIT_MACH_NO_FUSE=1 still
disables every fusion family.
Results
Each timing below is a median of three in-process runs in a fresh process,
except record_field’s primary row, which is a median of seven. Counting is a
separate untimed run. Both arms use the same executable, mcode, and serialized
Mach blob; only PIT_MACH_NO_GUARD_FUSE changes. Results matched exactly.
| benchmark | fusion off, dispatches | fusion on, dispatches | dispatch delta | fusion off ms | fusion on ms | time delta |
|---|---|---|---|---|---|---|
| record_field | 106,000,429 | 86,000,367 | -18.9% | 123.44 | 108.68 | -12.0% |
| fannkuch | 268,248,802 | 232,394,870 | -13.4% | 308.20* | 294.27* | -4.5% |
| spectralnorm | 266,116,632 | 227,632,186 | -14.5% | 301.08* | 280.86* | -6.7% |
| mandelbrot | 36,900,635 | 36,900,629 | negligible | 100.99 | 102.85 | noise / no effect |
| arith_int | 21,000,030 | 21,000,024 | negligible | 19.47 | 19.66 | noise / no effect |
* Average of two independent process medians. Fannkuch trials improved
5.3% and 3.7%; spectralnorm trials improved 6.2% and 7.2%. Record_field’s
additional shorter trials were noisier but all improved (4.8%-15.7%).
The exact results were record_field 974489, fannkuch 3000008629,
spectralnorm 1623647009, mandelbrot 1970674, and arith_int 144 in both
arms. The serialized blob sizes were also identical in both arms:
- record_field: 40,296 bytes
- fannkuch: 30,760 bytes
- spectralnorm: 88,760 bytes
The speed improvement is smaller than the dispatch reduction because the predicate still executes and record/array/numeric work remains. The important result is that this removes 20.0M, 35.9M, and 38.5M dispatches respectively at essentially no dynamic-memory cost.
Memory and code-size tradeoff
There is no per-actor and no per-loaded-code allocation. The in-memory instruction word count is unchanged; no sidecar was added. The representation adds one process-global cached A/B flag (4 bytes before alignment), six opcode names/table entries, and handlers.
Compared with the release library built from the campaign base:
- dylib file size: 1,454,104 -> 1,454,152 bytes (+48 bytes after Mach-O layout);
__text: 1,029,892 -> 1,030,876 bytes (+984 bytes);__cstring: +121 bytes, constant table: +48 bytes, data: +8 bytes;- aligned
__TEXTsegment and total virtual segment size were unchanged.
This is a good constrained-console trade: no actor-density penalty, no code object penalty, no serialized program penalty, and about 1 KiB of executable text for 4%-12% on the affected composed workloads.
Correctness receipts
make: release build passed../pit test tests/compile.cm: 64/64.- first cold
./pit test: 832/833; the sole failure was the known realization-chargedvm_suitetimeout. - immediate isolated
vm_suite: 1086/1086. - warmed full
./pit test: 1918/1918. ./pit fuzz 500 --seed 20260713: 3733/3733 generated checks.- new
mach_guard_fusioncross-lane regression: Mach290, native290. git diff --check: clean.
The new C handlers allocate no Pit heap objects. Pit_NewBool is an immediate
constructor, so no GC root is required by the C rooting rules.
Inline-splice fact audit
The other proposed seam was restoring typed handlers in functions marked
_has_inline_splice. Today one inline splice marks the whole host. Mach
lowering then discards both _write_types and _no_overflow for the whole
function, even though streamline re-optimizes it after each inline round. This
is deliberately coarse and loses typed arithmetic/comparison handlers.
I tested removing that gate, but did not retain the change. A previous composed
fannkuch run caught a real add_iip contract violation (a supposedly integer
slot held a non-integer after splice). The current debug/ASAN attempt could not
complete the composed census before the CLI’s two-second auto-reap killed the
benchmark actor, so it produced no new proof that the historical defect is
gone. Restoring facts without a successful debug-contract corpus is not an
acceptable performance trade. The safe next step is exact post-splice
definition/use facts (or SSA value facts), not removing the guard globally.
Tooling findings
The opcode census now also reports a three-run median, serialized Mach byte
size, and the top 24 opcodes. CENSUS_LONG=1 selects seven timing runs and
CENSUS_QUICK=1 skips timing for debug-contract probes.
Three existing workflow defects materially slowed this experiment:
- the bare-program two-second help probe/auto-reap kills long benchmark and debug runs even when the actor asks to remain alive;
- the worktree daemon endpoint disappears after several commands, forcing
fresh
PIT_INPROCESS=1processes for reproducible measurements; - AGENTS says
./pit test fuzz, but the command rejects that form and directs the user to./pit fuzz.
The typed one-shot CLI redesign already approved for the project is the right fix for (1). The POSIX lifecycle work diagnoses (2) but does not yet cure the listener loss. The native record_field lane also has a baseline QBE type error; the census path was therefore used for the Mach A/B, while the new small golden regression verified Mach/native agreement.
Recommendation
Keep and integrate the guard/branch fusion. It is target-internal, exact, measured on both micro/composed programs, nearly free in memory, and leaves unaffected programs unchanged. Keep inline-spliced typed facts disabled until their provenance is made use-site exact and a debug-contract corpus passes.
Source: plans/archive/perf-2026-07/perf-campaign-mach-guard-fusion.md