M6: immediate selection and validated scalar stack caching
Archived: a record of finished work, kept as written. nanofil's archive lists the others.
Retired (October 2026).
--registers=local, its cache (amd64_cache.pl) and its checker (amd64_cache_check.pl) were removed after the code-quality campaign (perf.md, register modes). Whole-function register homes with register operands and frame cells (--registers=temps, the-Odefault) do everything the cache did, on all eleven gate workloads, with fewer instructions and in less time (lz4-1within noise): geomean 3.32× filcc-O2for-O --registers=local, 2.18× for-O. The cache's only trusted code of its own was its checker, so keeping it cost about a tenth of the Nix check and ten program builds for a mode nothing used. Immediate selection (below) is not retired: it is the allocating selection,sel(local, …)infilc_amd64.pl, that temps emits. A retired--registers=localis refused with an error. The diagnostictests/bench-registers.plwith its kernelbench-registers.c, and the checker's attack suitetests/cache_check.plwent with it; they are at7fc3fa2. The rest of this page is the record of the milestone as it was.
--registers=local selects integer immediates, then
caches scalar stack homes. It is not general register allocation or
completion of M6. --registers=stack remains the default.
Both work independently of -O; build harnesses can set
NANOFIL_REGISTERS=local instead. The plain SysV backend is
unchanged.
Immediate selection precedes the cache's proof boundary
The opt-in amd64:compute_op//5 selects immediate right
operands for w/l add, subtract, multiply, and/or/xor and
the three shifts. It leaves the reference compute_op//4
unchanged. Constants normalize modulo the operand width; a qword ALU
immediate must equal a sign-extended imm32. Thus 0xffffffff
can be immediate at w but not at l, where it
means positive 4294967295. All other cases retain register operands.
Shifts mask the constant count to 31 or 63 and retain even a zero-count
instruction; branches still test their operand explicitly, not the
shift's flags.
Selection removes only the constant's scratch move to RCX. It retains the LHS load, result width, operation, defined flags and result store. Variable counts, division/remainder, comparisons, floats, pointer flight operations, helpers and checks are unchanged. The Fil-C function context carries the mode; setjmp forces reference selection before any emission.
The translation checker validates caching of the selected stream, not old selection against new selection. Selection is trusted emitter code, tested through emitted forms and native/interpreter/clang comparisons. An unsupported machine form or original cache-bank use disables caching of the whole implementation, retaining selected-but-uncached code. This differs from setjmp, which bypasses both selection and caching.
The machine contract bounds the transformation
amd64_contract.pl describes known instruction
forms, including register families and widths,
explicit/implicit reads and writes, flags, memory and control/frame
effects. Division names its accumulator and high half, variable shifts
read cl, and partial writes retain their widths. Any
unknown original form or original use of the r10/r11 bank
selects the uncached selected implementation. Calls may clobber both
cache registers. Call argument reads are not enumerated: calls are
unconditional boundaries, and ordinary registers remain equal. This is
not a general call-liveness model for future allocation passes.
filc_amd64 supplies the original temporary classes and
placed physical homes before that information disappears. Every alias of
an eligible scalar home must be w/l. A home shared with
s/d, an unknown class, pointer payload/lower, phi shadow,
local payload/aux, or frame bookkeeping is not eligible. All eight bytes
stay together: incoming w words can have nonzero upper
bits. A cold 32-bit read fills all 64 bits; its register read still
zero-extends into the original destination.
Only the implementation body is transformed. Getters, adapters, function objects and origins retain their original lines and independent layouts. Every setjmp function is unchanged, including one with only unreachable setjmp sites. The middle end already preserves those complete functions.
Regions preserve all existing boundaries
A region ends before every branch, call, label, directive, terminal,
frame-base/stack-pointer change, and every memory operation other than
an exact eligible frame load/store. This includes leave,
before it restores the caller's frame. Labels begin
empty; no predecessor relies on fallthrough cache contents. Unknown
instructions are not merely barriers.
The conservative memory boundary covers partial/overlapping frame accesses, indirect addressing and local payloads without an alias analysis. Helpers inside pointer operations are ordinary call boundaries, regardless of whether they usually exit. Phi reads into shadows, phi writes, polling, rooted helper reloads and exception tests keep their original order.
amd64_cache.pl first removes loads whose complete result
is already in the destination register. It remembers one physical-home
association per register, invalidating it on register writes and
invalidating every association with a home on a store. A dword reload is
removable only when the destination's high half is already zero; a dword
association alone never justifies removing a qword reload. Boundaries
forget all facts, including zero-extension. Copies and partial writes
remain opaque.
Then it groups a home's remaining first-to-last references in each
region. A deterministic linear scan assigns profitable intervals to
r10, then r11; an overlapping interval under
pressure stays on stack. There is no eviction or next-use heuristic.
Every store updates the resident home version. A first read fills the
register, and the last reference writes back a dirty home. All inserted
and replacement instructions are flag-preserving moves; no arithmetic
instruction moves or changes.
This reduces memory transfers, not necessarily instructions. Two stack moves can become two register moves plus a final spill. The pass retains the frame allocation and every original scalar home.
The checker validates the final instruction list
amd64_cache_check:check_cache/3 derives eligible homes
independently and pairs the selected uncached and final cached streams.
Only the three permitted move forms may change; a load may also
disappear if its complete result equals the current ordinary register.
This compares with the reference home, not possibly
stale candidate memory. Every other line must be identical and ordered.
It symbolically tracks ordinary register families, full-width cache
values, and separate reference/actual home contents. Low/high halves
distinguish a 32-bit value from its full incoming word. Unchanged
arithmetic gives both streams the same fresh result, rather than
reimplementing x86.
All homes must agree before every boundary. The checker then forgets
cache contents and uses fresh common symbolic entry values. Sparse maps
avoid rescanning the whole frame at every boundary. Inserted frame
accesses require the original prologue and cannot follow
leave. Every fill must immediately replace the next
original load of the same home: establishing rbp alone does
not permit speculative reads before the stack-overflow check. A failed
check is a compiler error, not silent fallback. The checker shares
machine facts, not interval choices or claimed eligibility; it derives
eligibility from the emitter's trusted class/placement inventory. An
omitted load is consumed before trying a later fill's immediate anchor;
the fill itself never moves earlier. Matching is deterministic and still
checks raw input against final output, not only the intermediate
cleanup.
Validation and limits
The native differential, interop, GC, helper-exit, exception and setjmp suites run with both register modes and both middle-end modes. GNU Hello, zlib and Lua also build and run in all four combinations. The unchanged stack mode has 1,030 byte-identical snapshot outputs against the preceding correctness-fix commit.
Hand-authored checker cases reject omitted writebacks, stale values across calls/labels, stale versions after ordinary stores, truncation of high bits, changes to flags/failure registers/exception branches, float-shared homes, unknown forms, and original cache-bank use. In-memory checker mutations removing zero-extension or boundary synchronization are caught. The speculative pre-overflow-check fill was accepted before the anchored- fill rule and is now rejected. Backend tests establish actual integer/float slot sharing and reject a mutation that misclassifies the float aliases. Native mutations also exercise the previously scratch-only pointer-helper and exception regressions; see pizlonation.md.
The trust boundary remains the original emitter, complete class/placement metadata, the audited machine forms, the checker and the assembler. This does not prove the original compiler or Fil-C runtime correct. No pointer allocation, reduced rooting, check/poll removal, cross-region allocation, setjmp allocation or C++ unwinding is introduced.
Static savings cost instructions and compilation time
The initial cache, before redundant-load removal, was measured on 2026-10-25 in a Linux x86-64 orb (Xeon at 2.60 GHz), SWI 9.2.9 and Fil-C staging 0.687. Each CPU/RSS value is the median of three runs, alternating stack/local order. CPU seconds include frontend, validation, middle end and backend, but not module loading or printing. RSS is the whole process. This is a shared VM, not a dedicated benchmark machine. The checker time is included in local's total, not added to it.
All arrows below are stack → local. Frame counts are
static operands addressed through rbp, not measured dynamic
traffic. Instructions and ELF object text include unchanged adapters.
Generated inputs use tests/bigfn.py 1 300 and
tests/bigfn.py 2 1000; 00200 is the pinned
c-testsuite input used by the corpus gate.
| Input | Middle end | Frame accesses | Instructions | Object text, bytes | Eligible homes / changed regions |
|---|---|---|---|---|---|
| Generated 300 | off | 4,831 → 4,104 | 17,810 → 18,405 | 77,564 → 79,281 | 5 / 374 |
| Generated 300 | -O |
4,681 → 4,149 | 6,609 → 6,949 | 35,335 → 34,942 | 46 / 184 |
| Generated 1000 | off | 17,100 → 14,456 | 61,422 → 63,659 | 264,926 → 271,422 | 5 / 1,363 |
| Generated 1000 | -O |
15,916 → 13,951 | 22,251 → 23,399 | 117,172 → 115,634 | 52 / 573 |
00200 |
off | 9,407 → 6,647 | 23,941 → 25,293 | 96,810 → 99,834 | 6 / 774 |
00200 |
-O |
9,325 → 6,568 | 23,616 → 24,965 | 95,274 → 98,314 | 7 / 772 |
| Input | Middle end | Total CPU, s | Backend CPU, s | Local checker, s | Peak RSS, KiB |
|---|---|---|---|---|---|
| Generated 300 | off | 0.232 → 0.567 | 0.112 → 0.451 | 0.145 | 62,948 → 67,184 |
| Generated 300 | -O |
0.269 → 0.424 | 0.063 → 0.212 | 0.068 | 40,268 → 42,672 |
| Generated 1000 | off | 1.090 → 2.287 | 0.706 → 1.900 | 0.575 | 191,564 → 215,592 |
| Generated 1000 | -O |
1.061 → 1.546 | 0.285 → 0.780 | 0.203 | 99,956 → 117,364 |
00200 |
off | 0.799 → 1.225 | 0.296 → 0.690 | 0.181 | 105,964 → 116,568 |
00200 |
-O |
1.238 → 1.663 | 0.255 → 0.677 | 0.201 | 105,996 → 116,564 |
Controls: tests/c/basic.c removes 66/46 frame accesses
(off/-O), float.c removes 13/23, and
setjmp.c excludes all five setjmp functions in both modes
while changing only its ordinary helper functions. There were no
unsupported-form fallbacks in these six measurement inputs.
Reproduce each row with preprocessed C (no preprocessing needed for
bigfn.py output), then assemble and measure the emitted
object:
cd nanofil/src
SWIPL=/tmp/swipl/bin/swipl /usr/bin/time -f '%M KiB' \
../bin/nanofil --script ../tests/bench-registers.pl \
/tmp/input.i o1 local /tmp/output.s </dev/null
as -o /tmp/output.o /tmp/output.s
size /tmp/output.oThus the optimized 00200 removes 29.6% of frame accesses
but adds 5.7% instructions and 3.2% text. Compilation rises 34.3%. These
results justify keeping the cache opt-in; memory-access counts alone do
not justify enabling it by default.
Native Lua 5.4.7 measurements do not establish a speedup either.
Using the four binaries from the passing nanofil-lua gate,
five alternating runs per mode gave these median user CPU seconds, with
all outputs checked:
| Workload | Stack, off | Local, off | Stack, -O |
Local, -O |
|---|---|---|---|---|
| Integer loop | 1.313 | 1.322 | 0.588 | 0.603 |
| Table reads | 0.884 | 0.881 | 0.479 | 0.470 |
The stack/local ranges overlap in every pair; optimized integer runs
span 0.560–0.614 s versus 0.570–0.617 s. These are VM measurements of
two small workloads, not evidence of a general runtime improvement.
Programs passed as lua -e (no GC-stress environment for
timing):
local s=0
for i=1,2000000 do s=(s+i*17)%1000000007 end
print(s)local t={}
for i=1,1000 do t[i]=i*3 end
local s=0
for r=1,2000 do for i=1,1000 do s=s+t[i] end end
print(s)Redundant-load removal reduces instructions without widening allocation
This follow-up uses the same toolchain and orb. Its baseline is 76de0216,
not the original stack backend. It follows Oracle's smaller proposal:
remove proven reloads before interval planning, with no copy propagation
or arithmetic rewrite. Instruction reduction is measured on the final
stream: replanning intervals can add other fills and writebacks.
Upstream informed the boundary, not an allocation algorithm. At the pinned Fil-C source, Pizlonator leaves ordinary integer operations alone (FilPizlonator.cpp); LLVM runs ordinary cleanup around instrumentation (BackendUtil.cpp). Nanofil likewise keeps safety lowering separate from scalar cleanup. It does not adopt upstream KillUB's division semantics: original native arithmetic and faults remain unchanged.
Thirty GDB samples per Lua workload put 59 of 60 samples in
luaV_execute. Its assembly includes result/store/reload
sequences, but this coarse sampling does not attribute a percentage of
time to them. Thirty zlib text-compression samples landed in
longest_match (23) and deflate_slow (7);
longest_match also contains the same store/reload pattern,
among many pointer operations that remain untouched. lvm.c
below is Lua 5.4.7 with the gate's switch-dispatch flags. The kernel is
bench-registers.c,
two loop-carried unsigned 32-/64-bit chains with runtime count and seed,
not a dead-code benchmark.
Arrows are old local → new local, not stack → local.
Compiler numbers are medians of three alternating runs of
bench-registers.pl; assembly was assembled and measured
with size. Frame accesses count operands, not hardware
events. Small CPU differences on this VM are not definitive.
| Input | Middle end | Instructions | Frame accesses | Object text, bytes |
|---|---|---|---|---|
00200 |
off | 25,293 → 22,635 | 6,647 → 6,647 | 99,834 → 91,786 |
00200 |
-O |
24,965 → 22,307 | 6,568 → 6,568 | 98,314 → 90,266 |
| Integer kernel | off | 1,561 → 1,534 | 353 → 353 | 6,798 → 6,718 |
| Integer kernel | -O |
588 → 564 | 161 → 156 | 2,430 → 2,366 |
Lua lvm.c |
off | 154,700 → 154,121 | 40,280 → 40,271 | 678,112 → 676,368 |
Lua lvm.c |
-O |
71,836 → 71,110 | 27,523 → 27,516 | 323,328 → 321,104 |
| Input | Middle end | Total CPU, s | Backend CPU, s | Checker CPU, s | Peak RSS, KiB |
|---|---|---|---|---|---|
00200 |
off | 1.189 → 1.245 | 0.697 → 0.743 | 0.187 → 0.192 | 116,492 → 116,444 |
00200 |
-O |
1.666 → 1.621 | 0.653 → 0.677 | 0.183 → 0.179 | 116,600 → 116,640 |
| Integer kernel | off | 0.143 → 0.144 | 0.037 → 0.038 | 0.009 → 0.009 | 46,124 → 46,156 |
| Integer kernel | -O |
0.122 → 0.119 | 0.010 → 0.010 | 0.003 → 0.003 | 36,704 → 36,740 |
Lua lvm.c |
off | 6.095 → 6.377 | 4.908 → 5.188 | 1.296 → 1.301 | 641,652 → 641,652 |
Lua lvm.c |
-O |
4.090 → 4.049 | 2.077 → 2.019 | 0.595 → 0.583 | 346,720 → 215,784 |
The optimized 00200 now has 10.6% fewer instructions
than old local, and 5.5% fewer than stack's 23,616, without sacrificing
its frame-access savings. Lua's instruction reduction is only 1.0% in
its VM translation unit. Stack remains much cheaper to compile:
optimized lvm.c takes 2.547 s total and 0.625 s in the
backend, versus new local's 4.049/2.019 s.
The new unit cases failed before implementation. Independent checker fixtures cover unknown upper bits, partial and implicit writes, physical home overwrites, stale actual memory, every existing boundary, multiple omissions before a fill, and trailing omissions with dirty writeback. In-memory producer mutations losing zero-extension, home invalidation or division's implicit writes are rejected as compiler errors. Checker mutations losing zero-extension or restoring the premature fill cut are caught by negative and positive fixtures respectively. Fresh-process unit runs pass after these mutations. Stack still has 1,030 byte-identical snapshot outputs; both stack Lua binaries are byte-identical too.
Native benefit is clear in the arithmetic kernel, not generally
Seven alternating trials per executable, after warm-up, pinned to CPU 4. Timings are wall time; timed processes ran serially without GC-stress switches. Trials wait for a quiet VM and discard concurrent external CPU work. Counts were calibrated on the baseline to roughly 0.6 s and then held fixed for each old/new pair. All kernel outputs were checked; zlib binaries and inputs were fingerprinted after correctness validation. This is still a shared VM, not a controlled physical-machine benchmark.
These medians are again old local → new local. Kernel units are nanoseconds per iteration, zlib units milliseconds per uncompressed MiB, and Lua units seconds for the two programs above.
| Workload | Middle end off | -O |
|---|---|---|
| Integer kernel, ns/iteration | 44.075 → 42.475 | 9.909 → 8.773 |
| zlib text compression, level 1, ms/MiB | 273.296 → 266.662 | 114.897 → 112.861 |
| zlib text compression, level 6, ms/MiB | 563.046 → 555.639 | 293.175 → 289.760 |
| zlib text compression, level 9, ms/MiB | 732.069 → 725.146 | 429.673 → 419.551 |
| zlib random compression, level 6, ms/MiB | 563.721 → 579.481 | 204.177 → 194.865 |
| zlib text decompression, ms/MiB | 62.250 → 61.551 | 29.854 → 29.431 |
| zlib random decompression, ms/MiB | 18.477 → 18.301 | 13.895 → 13.821 |
| Lua integer loop, s | 1.301 → 1.267 | 0.622 → 0.623 |
| Lua table reads, s | 0.869 → 0.882 | 0.492 → 0.469 |
The optimized kernel's 11.5% reduction has nonoverlapping trial
ranges: 9.706–10.189 versus 8.550–9.084 ns/iteration. It is the clearest
native benefit. The unchanged stack control's .text is
byte-identical, yet its median moved from 10.831 to 10.380 ns/iteration
(4.2%); small differences must not be overinterpreted. The kernel was
validated on 30 count/seed pairs against Fil-C clang and an independent
masked-integer Python model, in all modes normally and with GC
scribbling/verification.
Zlib's optimized medians improve 0.5–4.6%, but every old/new
range overlaps. Unoptimized random compression gets slower. Lua
table reads improve 4.7% by the median (ranges 0.471–0.522 versus
0.441–0.486 s), while the integer loop does not improve. These runs do
not establish a general application speedup. Keep
--registers=local opt-in; instruction selection and wider
value retention need their own measured justification.
Zlib is the gate's 1.3.2, all 17 objects compiled through nanofil.
Both labels and both register modes were timed with and without
-O; each also passed upstream tests and byte-identical
compression/cross-inflation against Fil-C clang normally and under GC
stress. Timing includes process startup and I/O. Each input is exactly 1
MiB: text concatenates sorted nanofil/src/*.pl, then sorted
nanofil/docs/*.md at the baseline commit, repeats and
truncates to size; random concatenates SHA256 digests of
b"nanofil-integer-bench-v1:" + i.to_bytes(8,"little"),
starting at zero. Their SHA256s are
f4bf31d7cd2abe0044a5f375b72e6f630d808616f7e49fa7c3ddcf25c7278b60
and
c60e5804f983537ce7e87bc82442f4118119d0e90066b3a3ea085ed7e2dc48eb.
Compression runs minigzip -1|-6|-9; decompression runs
minigzip -d on the level-6 reference output. Repeat whole
processes to reach the calibrated duration, dividing elapsed time by
repetitions.
For the kernel, compile bench-registers.c
in the desired modes, then run with seed 123456789. The
optimized local pair used 42,809,000 iterations per trial. The usual
driver can reproduce the build (from nanofil/src, with
SWIPL and FILCC set):
../bin/nanofil -O --registers=local cc ../tests/bench-registers.c \
</dev/null > /tmp/kernel.s
as -o /tmp/kernel.o /tmp/kernel.s
"$FILCC" -o /tmp/kernel /tmp/kernel.o
taskset -c 4 /tmp/kernel 42809000 123456789Immediate selection: fewer instructions with the same frame traffic
This experiment's baseline is a3c1269,
including redundant-load removal, not the original stack backend. Oracle
reviewed the immediate selector's width rules, scratch-register contract
and proof boundary before adoption. Its two-operand immediate
imul form was assembled and disassembled: GNU as encodes
the accumulator as both source and destination, without an RDX
result.
The previously sampled hot functions contain actual adjacent
constant-to- RCX moves followed by arithmetic or shifts through CL. In
luaV_execute there are 1,421 encodable arithmetic pairs and
707 shifts; longest_match has nine and one; the kernel loop
and increment have six and ten. The candidate converts all of them,
leaving unencodable qword constants alone. These counts were checked
independently in the assembled objects. They describe entire function
implementations, not dynamic execution counts.
Hot function, local -O |
Instructions, old → new | Function text, bytes |
|---|---|---|
luaV_execute |
47,454 → 45,326 | 220,451 → 210,657 |
longest_match |
2,067 → 2,057 | 9,877 → 9,831 |
Kernel main |
355 → 339 | 1,498 → 1,439 |
The reduction is 4.5% of Lua's VM instructions but only 0.5% of the
match function. Lua still contains 240 constant shifts by zero and
constant-mask expressions; removing/folding those is outside this
selection rule. The fixed Fil-C clang reference object instead extracts
an opcode with and $0x7f,%eax. It has 18,949 VM
instructions, versus Nanofil's 45,326; that remaining gap includes many
transformations, not just allocation. Separate clang builds were not
instruction-identical, for an uninvestigated reason, so comparisons use
one fixed baseline reference executable/object.
Compiler measurements below use the existing
bench-registers.pl, three alternating runs per input, the
same preprocessed input for both compilers, and the same SWI host.
Counts here cover the whole translation unit, including adapters and
other functions, unlike the function table above.
Input, local -O |
Instructions | Frame operands | Object text, bytes | Total CPU, s | Backend CPU, s | Checker CPU, s |
|---|---|---|---|---|---|---|
| Kernel | 564 → 548 | 156 → 156 | 2,366 → 2,302 | 0.084 → 0.082 | 0.015 → 0.015 | 0.0029 → 0.0028 |
Lua lvm.c |
71,110 → 68,801 | 27,516 → 27,516 | 321,104 → 310,352 | 4.088 → 4.137 | 2.071 → 2.071 | 0.605 → 0.589 |
zlib deflate.c |
56,967 → 56,642 | 17,973 → 17,973 | 251,986 → 250,050 | 2.596 → 2.575 | 1.463 → 1.477 | 0.390 → 0.395 |
The new emission cases failed on the old selector. Native regressions compare arbitrary-high-bit arithmetic with the interpreter and clang; negative and width-sized shift counts use IR, not undefined C shifts. In-memory mutations that truncate every qword constant to imm32 or mask qword counts with 31 fail both emission and native checks. A fresh process then passes all 77 unit tests. The cache checker is unchanged and does not catch those selector mutations: they change its trusted input too.
All four Nix gates pass, including both register modes and middle-end settings, GC-stressed pointer helpers and exceptions, and the Lua/zlib suites. Each actual-helper audit sees 20 ensure-aux exits and 20 store barriers. The default backend has 1,032 byte-identical snapshot outputs against the baseline. Across the benchmark builds, all 108 stack-mode objects have identical disassemblies and relocations at both optimization levels, providing unchanged-code timing controls.
Native timings used seven alternating serial rounds on CPU 4 of the same shared orb, after builds and compiler measurements finished. Every old/new pair used identical calibrated counts and frozen inputs. The runner checked kernel/Lua outputs, input and binary hashes, and the unchanged-stack object controls; zlib's bytes were checked before timing. Two trials were discarded and repeated for competing CPU load, leaving 567 accepted samples plus 81 warmups. These are elapsed times including startup and I/O, not hardware instruction counters. Brackets below are observed min–max, not confidence intervals. Arrows compare old local with immediate-selecting local.
Workload, local -O |
Old median [range] | New median [range] | Time change |
|---|---|---|---|
| Kernel, ns/iteration | 8.7942 [8.4391, 8.8907] | 7.0960 [6.8581, 8.0696] | -19.3% |
| Lua integer, s/million iterations | 0.3109 [0.2969, 0.3189] | 0.2956 [0.2818, 0.3154] | -4.9% |
| Lua table, s/million inner iterations | 0.2357 [0.2137, 0.2445] | 0.2247 [0.2102, 0.2377] | -4.6% |
zlib text compress -1, ms/MiB |
111.6900 [110.0963, 130.8207] | 112.0922 [108.9439, 129.4319] | +0.4% |
zlib text compress -6, ms/MiB |
290.3313 [285.7102, 296.8983] | 291.1059 [281.1944, 304.0976] | +0.3% |
zlib text compress -9, ms/MiB |
415.6940 [411.5860, 464.1817] | 422.0156 [408.2263, 507.7963] | +1.5% |
zlib random compress -6, ms/MiB |
198.2654 [189.9423, 206.9285] | 193.5963 [191.1560, 196.3179] | -2.4% |
| zlib text inflate, ms/MiB | 31.0199 [28.7420, 34.4919] | 30.9246 [28.3407, 34.1656] | -0.3% |
| zlib random inflate, ms/MiB | 14.1400 [13.9234, 14.4706] | 14.1293 [13.8401, 14.7812] | -0.1% |
The kernel's ranges do not overlap, while its unchanged optimized
stack control moves only 10.2638 → 10.2752 ns/iteration. Without
-O, local's kernel also improves: 43.9164 [42.8546,
49.0693] → 41.1927 [40.4790, 42.0015] ns/iteration, or 6.2% less time.
This establishes a native benefit for the arithmetic-chain workload, not
for arbitrary C programs.
Lua's ranges overlap in both workloads. Its unchanged optimized stack
controls move 0.3055 → 0.3168 and 0.2416 → 0.2396 s/million iterations,
showing the noise floor. Without -O, local medians improve
4.8% and 2.6%, again with overlapping ranges. These are promising
observations, not an established general Lua speedup. Every zlib pair
overlaps too, including without -O, where local median
changes range from -1.5% to +1.2%. The byte-identical optimized stack
controls themselves move by -3.2% to +1.0% across zlib workloads. There
is no clear zlib benefit or regression. Local mode remains opt-in; fewer
static instructions alone do not warrant changing the default.
To reproduce the strongest pair, build the unchanged integer kernel
at the baseline above and at this revision using the earlier build
commands, then alternate seven timed runs of each executable with
54882276 123456789, pinned to CPU 4. Expected stdout is
54882276 4bc602bc fa7a402963506b80; divide elapsed seconds
by 54,882,276. For Lua, use the earlier scripts with 1,820,540 integer
iterations and 2,280 outer table iterations (expected results 125355986
and 3423420000), building all 34 Lua 5.4.7 objects with the gate's
switch-dispatch flags. Optimized local zlib repetitions are 6/3/2 for
text compression levels 1/6/9, 4 for random compression, and 19/34 for
text/random inflation. The text recipe is the same as above but uses
this experiment's baseline; its SHA256 is
ad941c3c7d5e28031b4a48de7a0c00ccb13185ef6c7b947e7d73bacdf5d1b3c0.
The random input and its hash are unchanged. All compared builds use the
same inputs and one fixed Fil-C clang reference, with no GC-stress flags
during timing; correctness runs separately cover GC stress.