Archive — history, not state. Kept for its reasoning and its evidence; its plan is closed.
Native guarded-array store experiment
Date: 2026-07-13
Baseline: 2601786e
Branch: codex/perf-barrier
Review units:
ec37b75b— exact type spans and guarded-array-store performance change;550c1ae4— independent missing-barrier correctness fixes and nursery regression fixture.
Outcome
Keep the final experiment. QBE now consumes exact final-instruction type facts
to lower a contractually guarded array store directly. It removes the hot
__store_dynamic_ss -> __store_index_ss helper chain and omits the
generational write barrier only when the RHS is necessarily an immediate
value. No mcode opcode, Mach instruction, runtime frame field, or serialized
artifact field changed.
Paired A/B measurements use the same compiler and type facts, changing only whether the guarded-store lowering is enabled. Each lane has 3 warmups and 11 timed samples; the median is reported.
| benchmark | disabled native | enabled native | delta | disabled/ enabled asm | asm delta |
|---|---|---|---|---|---|
barrier_int_store (2M stores) | 22.351 ms | 16.034 ms | -28.3% | 29,019 / 29,592 B | +1.97% |
fannkuch | 249.690 ms | 206.072 ms | -17.5% | 116,513 / 120,825 B | +3.70% |
spectralnorm | 172.857 ms | 173.753 ms | noise; identical code | 256,300 / 256,300 B | 0 |
Mach is byte-for-byte unaffected. Paired fannkuch Mach medians were 308.525 and 308.241 ms. The isolated store medians varied between 22.97 and 23.66 ms.
Final correctness/smoke rows also matched across Mach/native:
stone_write: expected 2047;barrier_gc_arrays: expected 261633;array_grow: expected 149903 (2.59 ms focused native repeat; 2.67 ms in the focused matrix, versus the 2.52 ms baseline; hot emitted path is unchanged and the difference was not repeatable);array_read: expected 630533;mandelbrot: expected 1970674, native 27.944 ms;binarytrees: expected 674478, native 60.231 ms.
record_field and d_field still disrupt in native with the pre-existing
QBE numeric/remainder slot-type bug. Baseline and optimized d_rec3 emit
identical IL/assembly (36,682/28,707 bytes). This branch does not claim a
record benchmark win.
What changed
Exact type spans
Slot compression intentionally coalesces all non-int logical slots. Its old
function-global _write_types conflict merge therefore loses useful array,
numeric, and boolean facts, and can be unsafe if a consumer mistakes one
logical lifetime for another.
Compression now transports every non-null logical write-type over its exact final live interval as a flat sidecar:
_type_spans = [physical_slot, first_instruction, last_instruction, type, ...]
The QBE emitter queries physical slot plus current final instruction index, then uses the conservative global merge only when no span applies. Linear-scan allocation permits physical-slot reuse only for non-overlapping intervals. Restreamlining clears the sidecar, and canonical mcode serialization strips it.
The flat layout is deliberate. On the current 64-bit host its exact live owned
footprint per function is 16 + 8 * next_pow2(4 * span_count) bytes (minimum
array capacity 2). It allocates no per-span array and reuses existing type
texts. Measured totals:
| input | spans | exact live sidecar-array bytes |
|---|---|---|
barrier_int_store | 19 | 1,040 |
fannkuch | 134 | 8,208 |
spectralnorm | 334 | 18,672 |
mandelbrot | 37 | 2,064 |
binarytrees | 122 | 5,168 |
These bytes exist only while the compiler retains optimized IR. They do not
enter .mcode, Mach code, a native data segment, an actor frame, or the target
runtime heap. Tests cover mixed non-int reuse and the concrete dangerous
float/null/bool reuse shape.
Guarded array store lowering
docs/spec/mcode.md defines store as guarded: mcode checks receiver type,
key type, mutability, and bounds before the operation, and streamline removes
only proven-redundant guards. When the exact fact says the guarded receiver is
an array, QBE now:
- decodes the already-validated integer index;
- checks capacity because
index == lengthis an append; - updates length when appending;
- stores directly;
- calls the write barrier only for pointer/unknown RHS values;
- routes capacity growth or a stale forwarded alias to the existing fully
checked
__store_index_sshelper.
Routing forwards to the slow helper rather than inlining the chase reduced
fannkuch assembly from 123,159 to 120,825 bytes and improved its median from
222.49 to 206.07 ms. Disassembly of final fannkuch shows that its direct
numeric array-store blocks contain neither __store_dynamic_ss nor
pit_rt_write_barrier calls. pit_main still contains barriers for unrelated
pointer and cold diagnostic writes; seven __store_index_ss calls remain only
as uncommon growth/forward tails.
The nonpointer classes are int, num, float, bool, and null. All are
immediate in the current nan64, f32, and fixed representations. Pointer-capable
text/array/record/function/blob or unknown values remain barriered.
Correctness barriers fixed
The audit found two generated pointer-write paths that bypassed the generational barrier:
- inline array
pushwhen capacity was already available; putinto an outer/captured frame that may live in old space.
Both now emit a barrier for pointer/unknown RHS values and omit it for exact
immediates. These are correctness changes, not benchmark claims. The focused
native GC fixture grows arrays and a captured frame into old space, performs
replacement stores, growing pointer pushes, and captured-frame pointer writes,
retains a self-reference, and generates enough garbage for 17 verified minor
collections at an 8 KiB nursery. It passes with
PIT_GC_NURSERY_VERIFY=1 and result 261633.
The normal shop cannot run that verifier today: untouched boot code has a
separate missed remembered-set edge and aborts in internal/shop_actor before
the fixture. native_barrier_test therefore loads an already-built dylib in a
fresh raw context, isolating generated native code from shop boot.
Rejected designs
The first experiment passed a nonpointer flag through shared store helpers and branched around the barrier at runtime. It barely moved the isolated store (about 22.65 to 22.15 ms) and regressed fannkuch from 244.05 to 251.77 ms. That implementation and its signature changes were deleted.
The first direct version repeated all receiver/key/stone/forward checks at every site. It reached about 16.56 ms on the isolated loop and 222.49 ms on fannkuch, but increased fannkuch assembly about 5.5%. Consuming the existing guarded-store contract and outlining forwards produced the final 17.5% macro win at +3.7% assembly.
Validation
make: pass; manual native runner built.- targeted compiler tests: 70/70.
- warmed default suite: 1,918/1,918.
- isolated VM suite: 1,086/1,086.
- fuzz: 740/740, seed 527089.
- native nursery verifier: expected 261633, 17 minor collections, pass.
- focused Mach/native matrix: stone, replacement/growing/self-reference arrays, array growth and reads all match their golden results.
git diff --check: pass.
The first default-suite attempts exposed known tooling problems rather than
test failures: sandboxed loopback binds fail, and the 30-second test deadline
starts before cold realization. vm_suite took about 32 seconds to build and
then 0.9 seconds to run. Temporarily raising the deadline allowed the cold
artifact to build; restoring 30 seconds and rerunning the warmed suite was
fully green. No timeout semantic change is retained here.
Tradeoff decision
The accepted runtime tradeoff is native text size only: +573 bytes on the focused store module and +4,312 bytes on fannkuch. Runtime heap and frame size are unchanged. Compiler-only sidecars are small, exact, flat, and absent from artifacts. A 17.5% composed-program win and 28.3% isolated store win justify the 2-4% code growth; programs with no qualifying store, such as spectralnorm, emit identical native code.
Follow-up seam for Mach
_type_spans is indexed by final instruction number and compressed physical
slot, after the instruction stream and slot allocation have stabilized. That
is the shape Mach needs to query exact load/store types at lowering time. In a
separate experiment it can replace the current _has_inline_splice blanket
fact disable with point-specific lookup, potentially restoring typed handlers
after inlining. This branch deliberately makes no Mach lowering change, so the
native result and that prospective Mach experiment remain independently
reviewable.
Source: plans/archive/perf-2026-07/perf-native-array-store.md