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:

  1. runs the identical predicate;
  2. writes the identical boolean result slot (it is not treated as dead);
  3. consumes the original branch word and applies its original relative offset;
  4. 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.

benchmarkfusion off, dispatchesfusion on, dispatchesdispatch deltafusion off msfusion on mstime delta
record_field106,000,42986,000,367-18.9%123.44108.68-12.0%
fannkuch268,248,802232,394,870-13.4%308.20*294.27*-4.5%
spectralnorm266,116,632227,632,186-14.5%301.08*280.86*-6.7%
mandelbrot36,900,63536,900,629negligible100.99102.85noise / no effect
arith_int21,000,03021,000,024negligible19.4719.66noise / 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 __TEXT segment 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-charged vm_suite timeout.
  • immediate isolated vm_suite: 1086/1086.
  • warmed full ./pit test: 1918/1918.
  • ./pit fuzz 500 --seed 20260713: 3733/3733 generated checks.
  • new mach_guard_fusion cross-lane regression: Mach 290, native 290.
  • 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:

  1. the bare-program two-second help probe/auto-reap kills long benchmark and debug runs even when the actor asks to remain alive;
  2. the worktree daemon endpoint disappears after several commands, forcing fresh PIT_INPROCESS=1 processes for reproducible measurements;
  3. 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