Archive — history, not state. Kept for its reasoning and its evidence; its plan is closed.

Compiler optimization campaign: final performance matrix

Status: final integrated campaign seed measured and validated. External-runtime lanes are the earlier same-machine comparison run; Pit lanes below are the clean-daemon final branch measurements.

Date: 2026-07-14

Branch: compiler_optimizing

Committed base: da2a46b5901ac3f56ae9ba34b0550b832c90fa1f

Machine: Apple arm64 (Darwin 25.5.0, RELEASE_ARM64_T6050).

This matrix is the composed end-of-campaign measurement, not a collection of best historical numbers. It keeps dynamic/interpreted VM comparisons separate from native/JIT comparisons and verifies each workload result in the harness.

Interpretation groups are:

  • Pit Mach, Lua, QuickJS, CRuby, and mruby are VM/interpreter comparisons.
  • LuaJIT, Node/V8, Bun/JavaScriptCore, and .NET/RyuJIT are optimizing-JIT comparisons; they are the relevant aspirational field for hot AOT output, but they may use runtime feedback and allocation sinking unavailable to a cold static build.
  • Pit native and ocamlopt are AOT comparisons. OCaml is compiled without a flambda flag in this matrix.

The table is deliberately about language workloads rather than identical object representations. JavaScript/Lua/Ruby use their normal language record objects. The statically typed ports use Dictionary/Hashtbl for the honest generic string-keyed record rows; a C# class or OCaml record would instead be a static offset-layout ceiling. Pit’s new dense shapes mean record_new now sits between those categories, while the polymorphic d_field and record_field sites remain the more useful generic-access comparisons.

Method

The comparison sources are aot_bench/cross/bench.*, aot_bench/cross/shootout.*, and aot_bench/cross/calls.{erl,exs}. Each micro/shootout program uses its existing calibrated harness: run the workload once for an oracle, then for each of three inner trials double repetitions until at least 50 ms and retain the best normalized time. This campaign executed each complete harness three times serially and reports the median of the three reported per-row values. Thus every table entry is a median of three independent calibrated best-of-three values. No benchmark programs ran in parallel.

Exact commands, each repeated three times:

lua aot_bench/cross/{bench,shootout}.lua
luajit aot_bench/cross/{bench,shootout}.lua
qjs aot_bench/cross/{bench,shootout}.js
node aot_bench/cross/{bench,shootout}.js
bun aot_bench/cross/{bench,shootout}.js
ruby aot_bench/cross/{bench,shootout}.rb
mruby aot_bench/cross/{bench,shootout}.rb
ocamlopt -O3 -o /private/tmp/pit_cross_{bench,shootout} aot_bench/cross/{bench,shootout}.ml
/private/tmp/pit_cross_{bench,shootout}
dotnet run --file aot_bench/cross/{bench,shootout}.cs -c Release
erlc -o /private/tmp aot_bench/cross/calls.erl
erl -pa /private/tmp -noshell -s calls main
elixir aot_bench/cross/calls.exs

Runtime versions: Lua 5.5.0; LuaJIT 2.1.1783773675; QuickJS 2026-06-04; Node 26.5.0 / V8 14.6.202.34; Bun 1.3.14; CRuby 2.6.10p210; mruby 4.0.0; OCaml 5.5.0 native (ocamlopt, no flambda flag); .NET SDK/runtime 10.0.301; Erlang/OTP 29 with BEAM JIT; Elixir 1.20.2 on OTP 29. s7 is not installed.

The macOS /usr/bin/time -l facility could not read kern.clockrate inside the benchmark sandbox and did not emit peak RSS. Cross-runtime peak memory is therefore unavailable in this run. Target heap/allocation/GC evidence for Pit comes from vs.ce’s actor statistics instead.

Cross-runtime micro suite

Milliseconds; lower is better. These are the fourteen workloads currently implemented consistently in every bench.* source.

benchPit MachPit nativeLuaLuaJITQuickJSNodeBunRubymrubyOCaml.NET
arith_int25.7409.61026.13715.45143.4929.3089.96660.84573.3039.9308.075
arith_poly21.8007.43029.25410.82038.4806.7307.11960.93381.0097.0826.146
loop_nested24.10011.67035.18619.27448.15711.54112.36869.92086.79911.4769.394
float_math16.5702.3708.1441.22311.9471.2231.26126.35726.0471.2561.246
fib51.86013.61021.9682.75334.5534.1172.60938.62445.6842.0421.118
call_hot14.2207.86032.0609.41132.2765.7336.19247.61675.6116.6124.913
closure50.88026.24048.27211.44564.4427.4836.94283.668107.0108.1696.322
array_read24.18023.44021.64413.90836.2918.6079.29748.77366.77710.0007.384
array_grow2.9902.7102.4940.9782.8270.9090.5054.4322.4070.497
record_new21.18014.85031.6108.54324.3051.0581.19230.12540.43421.3029.523
string_concat0.4200.35011.0638.9280.5050.0390.04410.60534.9449.13327.430
gc_churn7.3006.15017.0014.96910.1240.3880.5388.30012.7324.6833.140
d_field22.77011.82013.5293.22015.3024.1952.69432.26546.52217.6307.916
record_field115.20052.41078.79121.94379.31524.39214.874166.201220.71197.21042.949

mruby 4.0.0 rejects the 300,000-element array_grow workload with array size too big; this is a missing result, not a zero or a timeout.

Cross-runtime composed shootouts

benchPit MachPit nativeLuaLuaJITQuickJSNodeBunRubymrubyOCaml.NET
nbody258.930150.79071.8583.06075.4491.8342.242145.548242.8811.5831.599
binarytrees78.79050.06064.89620.18843.2062.7943.28334.70231.7991.1922.820
spectralnorm167.77044.550181.1807.424281.9407.0053.891422.066379.85710.5393.491
fannkuch227.960111.350140.50017.265191.06512.0439.818447.382400.09413.63810.649
mandelbrot89.1509.21051.87241.64961.0442.2192.934140.832172.4033.8732.900

All runtime results matched the shared workload oracles.

BEAM call comparison

These are seven-sample medians inside each run, then the median of three complete process runs. dynamic_call0 measures indirect selection among eight zero-argument immutable BEAM closures. It is useful call-dispatch evidence but is not semantically identical to Pit’s mutable captured-cell closure fixture.

benchErlang/OTP 29Elixir 1.20
call_hot8.2538.173
dynamic_call026.05125.970
fib2.1942.259
tco_self4.0723.666

Pit-only extended matrix

The Pit harness performs two warmups followed by seven timed samples per lane and reports the median. The final serialized 68-row aggregate completed with check=Y for every row. The selected rows below are the clean-daemon medians; allocation and collection columns are shown where the final summary captured them. Unlike the earlier exploratory matrix, nbody now statically binds its math/radians.sqrt dependency and executes in both lanes.

benchcategoryMach msnative msallocation bytes Mach/nativeGC Mach/native
arith_intcompute25.749.61about 1 KiB0 / 0
arith_polycompute21.807.43about 1 KiB0 / 0
pow2_divremcompute44.0015.13about 1 KiB0 / 0
loop_nestedcompute24.1011.67about 1 KiB0 / 0
float_mathcompute16.572.37about 1 KiB0 / 0
intrinsic_opsbuiltin24.9213.88about 1 KiB0 / 0
call0_plaindynamic call45.3540.65about 1 KiB0 / 0
call_hotknown call14.227.86about 1 KiB0 / 0
closuredynamic closure50.8826.24about 2 KiB0 / 0
fibrecursion51.8613.61about 1 KiB0 / 0
tco_selftail recursion44.144.98about 1 KiB0 / 0
switch_chaindispatch59.8215.30about 1 KiB0 / 0
array_readmemory24.1823.44about 17 KiB0 / 0
arrfor_sumhigher order43.1224.97about 134 KiB0 / 0
record_fieldgeneric record read115.2052.41about 2 KiB0 / 0
pgo_record_field, unprofileddynamic shaped read38.1925.83about 2 KiB0 / 0
record_templateshaped construction34.1726.84
record_template_suspendshaped/suspending20.5916.12
record_nullable_shapeshape fallback3.964.80
mach_guard_fusionguard control11.3311.28
array_growallocation2.992.718,389,464 / 8,389,6083 / 2
record_newallocation21.1814.8516,817,096 / 16,817,26420 / 11
string_concatallocation0.420.35492,816 / 492,9120 / 1
gc_churnallocation7.306.159,625,416 / 9,625,60010 / 7
rec_allocrecursion/allocation6.664.724,800,744 / 4,800,7444 / 1
record_grow_keysstructural mutation0.120.23about 65 KiB0 / 0
native_call_genericC leaf14.828.81about 1 KiB0 / 0
native_callstatic-export C leaf11.668.39about 1 KiB0 / 0
native_nested_genericnested C leaf20.5214.74about 1 KiB0 / 0
native_nestednested static-export C leaf17.5113.49about 1 KiB0 / 0
d_loopdecomposition3.001.58about 1 KiB0 / 0
d_rec1decomposition2.444.45about 1 KiB0 / 0
d_rec3decomposition2.914.99about 1 KiB0 / 0
d_fielddecomposition22.7711.82about 2 KiB0 / 0
d_arr3decomposition15.3411.07
licm_resetcorrectness/perf canary9.241.53
native_stone_forwardstone canary13.5010.84
stone_fieldmemory7.703.89about 1 KiB0 / 0
mandelbrotshootout89.159.21about 1 KiB0 / 0
fannkuchshootout227.96111.35about 1 KiB0 / 0
spectralnormshootout167.7744.55about 25 KiB0 / 0
binarytreesshootout78.7950.06about 42.2 MiBabout 27 GC
nbodyshootout258.93150.79

The nursery remained disabled throughout this final matrix: all nursery allocation and collection counters were zero.

Current readout

  • Pure integer loops now land where intended: Pit native is essentially tied with OCaml/.NET on arith_int, arith_poly, and loop_nested; Pit Mach is competitive with or faster than Lua on those rows.
  • Known/inlined calls are also healthy: call_hot native is close to OCaml and faster than LuaJIT. Honest dynamic activation is not: the polymorphic zero-argument call0_plain costs 40.65 ms and the captured/dynamic closure row is 3.2x OCaml and 2.3x LuaJIT. The retained Mach outer-frame cache does help the closure row, but it does not change native activation. The BEAM indirect-zero-arg result around 26 ms is a useful directional comparison, although BEAM’s fixture has immutable closures and does less accumulator arithmetic than Pit’s fixture.
  • Boxed array traffic is a clear remaining native weakness. array_read is no faster native than Mach, 2.4x OCaml, and 1.7x LuaJIT. This agrees with the rejected exact boxed-array proof: check removal did not remove the dominant representation traffic.
  • Dense shapes materially fixed record allocation density: record_new allocates 16.8 MB rather than the prior roughly 43.2 MB and is faster than the ordinary Lua/QuickJS/Ruby lanes. It is still allocation-bound and far behind JS JIT object allocation. Honest polymorphic record_field remains 2.4x LuaJIT but now beats OCaml’s Hashtbl analog.
  • The shootouts identify different ceilings. Native mandelbrot is 4.5x faster than LuaJIT and within 2.2x of OCaml, so scalar numeric lowering is working. spectralnorm is 6.0x LuaJIT and 4.2x OCaml, dominated by calls and boxed array access. fannkuch is 18.5% faster than interpreted Lua, but remains 6.4x LuaJIT and 8.2x OCaml because of dense array/index mutation. binarytrees is slower than Lua and spends about 43 MB/27 collections; object allocation and collector behavior dominate it. nbody is an especially poor result (150.79 ms native versus 1.58 ms OCaml), showing that repeated array/record access and call boundaries still overwhelm otherwise healthy scalar arithmetic.

Movement from the contained campaign baseline

compiler-optimizing-handoff.md records the fresh contained-campaign baseline at 81685c37. It predates several mechanisms already present at this arc’s da2a46b5 starting point, so this table measures the full contained compiler journey rather than claiming every delta for the linked/shape/PGO arc alone.

benchmarkbaseline Mach/nativecurrent Mach/nativeMach changenative change
mandelbrot108.30 / 28.0189.15 / 9.21-17.7%-67.1%
fannkuch297.85 / 241.13227.96 / 111.35-23.5%-53.8%
spectralnorm305.97 / 172.10167.77 / 44.55-45.2%-74.1%
binarytrees97.66 / 60.7178.79 / 50.06-19.3%-17.5%
dynamic closure58.07 / 30.4350.88 / 26.24-12.4%-13.8%
Fibonacci54.24 / 28.7951.86 / 13.61-4.4%-52.7%
self TCO89.13 / 30.3044.14 / 4.98-50.5%-83.6%

The final same-artifact canary revises the earlier optimistic shape result. Dense/default binarytrees measured 74.79/47.25 ms and 43.25 MB, versus 83.63/44.86 ms and 53.96 MB for generic-template and 80.37/51.66 ms with shapes off; a generic repeat was 80.34/44.73 ms. Dense shapes are therefore a clear memory and Mach win and beat no-shape native, but they carry a repeatable approximately 5% native regression versus the generic-template representation on this one allocation-heavy workload. They remain retained because the shared representation cuts each common three-field record from 144 to 56 bytes, removes roughly 20% of binarytrees allocation traffic, and improves the default path on both lanes relative to no shapes. The native regression is an explicit follow-up target, not hidden as noise.

PGO production result and artifact identity

The final program-scoped profile run returned 3,200,000 in every mode:

modeMach msnative ms
ordinary53.87530.147
collect56.283
auto/consume45.95422.895

The generation-1 portable profile is 7,088 bytes. Its semantic hash is d28969b9ced728b4e4753a5c998de137131819c715560fe0c05d50042db1f91f and its content hash is 48c924e48b69c3e487cac489e4fc988a4ec3a2dcd4a8db8ce4bde1e2409af9a9. Consumption uses only record-load sites with at least 1,024 observations and 90% valid top-four shape coverage.

Final strict artifacts contain 65 validated mcode modules. Exact sizes are 74,842,189 bytes for boot.qop, 67,509,488 bytes for boot/firmware, and 1,573,945 bytes for boot/boot (143,925,622 bytes combined). Compared with the immediate pre-audit campaign artifacts, the combined result is 1,095,242 bytes smaller; the engine is 126,372 bytes larger because it contains the strict portable validator/hydrator.

The final SHA-256 identities are a58afba087c65d9d04b31c10ade9c16400fc3766b3a3f8a8295749462eeb7d57 for boot.qop, 797db28fc9b1bec61db89da36a5656049db9da3389ce72a6f0dc61c12b175e89 for boot/firmware, and 6fff594fcb6f130c9c2ab5db5c1f595081320ec56d1c7f6f11ea1ebb4bf17174 for boot/boot.

Integrated validation and memory hygiene

  • make seed passed; Meson passed 7/7; focused compiler tests passed 145/145.
  • The isolated VM suite passed 1,086/1,086 and the warmed full suite passed 1,921/1,921. One cold full attempt timed out only while charging realization to vm_suite; its isolated and warmed reruns passed.
  • Deterministic fuzz passed 3,733/3,733 checks over 500 programs with seed 20260713. The 68-row aggregate returned check=Y for every row.
  • Final validation found and fixed two general wrong-code hazards: LICM no longer treats branch-local/conditional writes as dominating replacements, and QBE now records helper-written is_array, is_func, and is_record predicate destinations as frame writes.

A clean node mapped 64 MiB: 4 MiB of actor heap, 1 MiB of node constants, and about 20 MiB of shared Mach code; the shop was about 2 MiB live in an 8 MiB block and the builder about 59 KiB live in a 256 KiB block. Running the entire aggregate and test fleet without restarting grew the node to 704 MiB mapped, 278 MiB of actor heaps, 37 MiB of node constants, and a 242/256 MiB builder block. That is compiler/test-daemon retention, not target benchmark memory: it slowed binarytrees to 216/136 ms and record_new to 70/90 ms while leaving call_hot near 15.5/6.67 ms. Restarting restored 78/48 and 23/15 ms. Final performance claims use only the restarted clean-daemon measurements.

Source: plans/archive/perf-2026-07/perf-campaign-final-matrix.md