Archive — history, not state. Kept for its reasoning and its evidence; its plan is closed.
Mach nullable-numeric decode experiment
Date: 2026-07-13
Base: 0da78edd
Branch: codex/perf-mach-guard-fusion
This is a separately reviewable Mach/runtime experiment. It changes no semantic mcode, no serialized Mach opcode, and no language rule.
Correctness defect
Every arithmetic expression is currently marked num by mcode even though
division by zero and non-representable results are observably null. That
allows a result to feed later add/subtract/multiply/divide without another
guard. Both backends then decoded tagged null as numeric zero.
The regression computes a dynamic 1 / 0, feeds it to all four operations,
and returns one bit for each result that remains null. The correct result is
15; before this experiment both Mach and native returned 0.
Raw and streamlined mcode agree: the downstream operations are emitted
without guards because their operand slot is considered numeric. This is not
a streamline transformation error. pit-compiler/resolve.cm even documents
the null-to-zero downstream coercion as current behavior, but it contradicts
the value contract that unrepresentable numeric results are null and permits a
failed numeric computation to become an ordinary non-null number.
Rejected repair: checks in every handler
The direct repair checked both operands for null in each generic Mach add/subtract/multiply/divide handler. It fixed the oracle, but spectralnorm’s same-period median regressed from 286.55 to 308.51 ms (+7.7%). Its float-heavy loop pays two extra tag tests on almost every one of 64 million arithmetic dispatches. That implementation was deleted.
Retained Mach repair
Mach mcode already guarantees that these handler operands are number-or-null. A small inline decoder now implements exactly that internal contract:
- tagged int -> direct int-lane-to-double conversion;
- tagged null ->
NAN, which is absorbing through hardware arithmetic and is mapped back toPIT_NULLby the existing non-finite result check; - short float -> direct representation decode.
The four generic handlers use this decoder instead of making eight
out-of-line Pit_ToFloat64 calls. The integer/integer fast path is unchanged.
This both fixes null propagation and removes call overhead from the normal
float path.
Mach performance
Each result is the median of three three-run medians from fresh processes. The baseline and experiment were rebuilt from adjacent trees in the same measurement period. Dispatch counts and serialized Mach sizes are identical.
| benchmark | baseline ms | experiment ms | delta | role |
|---|---|---|---|---|
| float_math | 18.82 | 14.60 | -22.4% | float arithmetic micro |
| spectralnorm | 286.55 | 263.16 | -8.2% | float-heavy macro |
| mandelbrot | 105.03 | 91.62 | -12.8% | float-heavy macro |
| fannkuch | 294.55 | 292.99 | -0.5% (noise) | integer/control macro |
Exact results remain float_math 525855, spectralnorm 1623647009,
mandelbrot 1970674, and fannkuch 3000008629.
There is no per-actor, per-frame, or per-code memory. The exact same-tree shared-library differential is:
- dylib file: 1,454,232 -> 1,454,232 bytes (unchanged);
__text: 1,029,284 -> 1,029,768 bytes (+484 bytes);- cstring, const/data sections, aligned virtual segments: unchanged.
Correctness and tooling receipts
- Dedicated compiler/Mach oracle: expected and obtained
15. - Standalone reproducer:
aot_bench/regress/null_numeric_launder.ce. aot_bench/dumpil.cenow acceptsDUMP_MODE=raw, making raw/streamlined comparison possible without editing the driver for every investigation.make: passed.- compiler tests: 67/67.
- first cold full suite: 832/833; sole failure was the known realization-time
vm_suitetimeout. - immediate isolated vm_suite: 1086/1086.
- warmed full suite: 1918/1918.
./pit fuzz 500 --seed 20260713: 3733/3733.
The new C helper allocates nothing and holds no heap PitValue across an allocation, so the GC-rooting checklist does not apply.
Native remains an explicit failing lane
This commit does not claim to repair native. The same standalone oracle still
returns 0 in native at this point. That is a pre-existing backend defect and
is being handled as a separate experiment so a native code-size or hot-path
regression cannot hide inside the Mach win.
A rejected native prototype made the global tagged numeric decoder map null
to NaN. It fixed native (0 -> 15) but expanded spectralnorm IL from 388,818
to 407,435 bytes (+4.8%), assembly from 257,196 to 269,552 bytes (+4.8%), and
its one-run same-period native time from 173.96 to 175.99 ms (+1.2%). It was
deleted.
The lower-cost native design under test is:
- preserve NaN/Inf absorbingly inside promoted raw-double (
fraw) regions; - decode nullable values only at tagged/fraw spill, reload, and write seams;
- for generic tagged arithmetic, combine the two exact-null predicates into one branch to a null result, and omit it when both operands are already raw or statically non-null.
That repair will be kept only if it fixes native 15 without a meaningful
regression on non-null float loops.
Recommendation
Keep the Mach change independently. It is a correctness repair and a large, repeatable Mach speedup with no dynamic-memory or bytecode cost. Review native separately; do not weaken or revert this Mach improvement merely because QBE needs a different representation-aware seam.
Source: plans/archive/perf-2026-07/perf-campaign-mach-nullable-numeric.md