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

Compiler optimizing experiment: restart handoff

Date: 2026-07-13

Branch: compiler_optimizing

Branch point for the contained performance campaign: 81685c37

Branch tip before this handoff: 7fa49065

This is the restart index for the compiler-performance experiment. It does not replace the detailed reports. It states what is actually present, what was measured at which checkpoint, what was rejected and deleted, what costs memory, and what remains only a proposal. Read this before starting another performance experiment so a discarded arm is not recreated without a new premise.

Final campaign update — 2026-07-14

This document began as the restart handoff and its lower sections preserve that historical checkpoint. The later linked/shapes/PGO arc is now complete. For current authority use, in order:

  1. perf-campaign-final-decisions.md for the retained/rejected inventory and tradeoffs;
  2. perf-campaign-final-matrix.md for clean-daemon final benchmarks and integrated validation; and
  3. perf-linked-pgo-arc.md for the chronological trial log.

The major work described below as “approved, not landed” did land: dense static record shapes, program-scoped PGO with a profitable record-load PIC, strict fact-free semantic artifacts plus versioned lowering sidecars, static export caching, synchronous Mach C-leaf invocation, native power-of-two division and remainder, and a Mach closure outer-frame cache. Final validation passed make seed, 7/7 Meson, 145/145 focused compiler, 1,086/1,086 isolated VM, 1,921/1,921 warmed full suite, and 3,733/3,733 fuzz checks. All 68 final benchmark rows returned exact Mach/native oracles.

Reading order and authority

  1. This file is the disposition and restart index.
  2. perf-campaign-2026-07.md is the detailed experiment ledger through the Mach/native array work and suspension prerequisite.
  3. Later accepted experiments have focused reports: perf-structured-tco-numeric.md, perf-structured-loop-phis.md, perf-spectral-inline-cleanup.md, perf-native-checked-store-region.md, perf-native-record-templates.md, perf-native-compact-raw-continuation.md, perf-native-fannkuch-loop-pressure.md, and perf-active-structured-midend.md.
  4. Failed call-frame reuse is preserved in perf-call-activation-cache.md.
  5. perf-next.md is valuable historical context, but its charts predate the final contained composition and must not be treated as the current matrix.

Source and generated-code inspection outrank recollection. A timing in this file is attributed only to the exact report which measured it. “Latest” below means the latest exact checkpoint currently recorded for that benchmark, not a single final matrix run at HEAD.

Scope and fixed constraints

The objective is generated-program speed, first native AOT and then Mach. Compiler speed is not an acceptance criterion. Generated code size, actor memory, heap traffic, continuation memory, and shared runtime text remain acceptance criteria because targets include 4 MiB-class systems and may extend to a deliberately reduced 288 KiB profile.

The experiment preserves these language/runtime constraints:

  • at most four parameters; missing arguments null-fill; excessive dynamic arguments disrupt;
  • parameters and def are immutable;
  • use() is top-level, literal, bound to def, and returns an actor-local stone module value;
  • mutable captures remain cells, not scalar SSA values;
  • record/array stone and deletion behavior remains observable;
  • numeric overflow, null, non-finite values, signed zero, and disruption behavior remain exact;
  • Pit calls retain a reifiable, relocating-GC-walkable continuation and can suspend; generated native code cannot rely on an unfreezable host stack;
  • semantic mcode remains the portable language-machine contract.

What is actually on this branch

The branch contains a composed sequence of compiler, Mach, runtime-correctness, benchmark, and tooling commits. The major retained mechanisms are:

Compiler correctness and fact identity

  • LICM constants are point/use-span facts, not unsafe function-global claims. Integer ranges and QBE consult the constant only at the exact use. Golden execution tests catch the case where a constant and a runtime divisor reuse one physical slot.
  • Integer range analysis is allowed on large functions. The old arbitrary 128-instruction cutoff was removed; a final one-million state-cell product guard now bounds pathological compiler memory while failing closed.
  • Exact type, constant, range, call, loop, and inline facts are transported or regenerated after instruction mutation rather than trusted at stale indices.
  • Mach refreshes rooted mcode instructions after a collection before it uses them again.
  • Native cache identity includes emitted IL, preventing stale binaries from masquerading as compiler results.

Native numeric and structured value work

  • Raw numeric regions keep canonical doubles and bounded integers out of tagged slots until an observable, call, capture, GC, disruption, or suspension boundary.
  • Immutable parameter inference and final range cleanup preserve exact facts through hoisted declarations and calls.
  • Self-TCO phis and selected natural-loop phis get stable value identities and raw representations.
  • The active read-only structured view now covers finalized mcode with blocks, normal/exception edges, dominance, loops, values, cells, effects, origins, and exact round trip. QBE consumes a deliberately narrow raw-value lane through safe single-predecessor regions.

These changes are general compiler proofs. They do not recognize benchmark names. The largest scalar wins therefore apply to programs with the same semantic shape, not only to the supplied sources.

Arrays

  • Native consumes exact array receiver/index facts, shares exact read handling, keeps selected numeric results raw, and folds compiler-generated checked load/store regions.
  • Mach has typed index handlers, local integer-proof consumption, allocation-free existing-capacity stores, an inline pure read helper, and a fused compiler-proven checked-store region.
  • Bounds, append/growth, null-on-read, disruption-on-invalid-write, write barriers, and forwarding remain in the selected contracts.

This is not typed-array storage. Arrays still hold ordinary PitValues. PGO or static proofs can remove guards and materialization, but a future unboxed array backing store would be a separate representation decision.

Records

  • Known-field records are sized against the table’s actual load limit. On binarytrees this cut allocation traffic by 44.4%, collections from 62 to 33, and time by 12.1% Mach / 12.9% native in the controlled experiment.
  • Hot native literal key sets can use shared preseeded descriptors. This makes construction use fixed-offset stores while instances remain ordinary hash records. It improved record_new 21.55 -> 14.04 ms (-34.8%) and binarytrees 52.62 -> 44.95 ms (-14.6%).

Shapes are not implemented. Record templates accelerate construction but do not shrink the ordinary three-field instance or make general field access an offset operation.

Calls, frames, and suspension

  • Native AOT execution now has cooperative, freeze-safe suspension and exact cross-engine resume/disruption handling. This is correctness infrastructure with a measured normal shootout tax usually around 1-2% and a larger recursive-call tax.
  • Self-tail calls remove their dead callee lookup after TCO.
  • One narrow raw Fibonacci convention proved that exact continuation layouts matter: private frames fell from 104 to 56 bytes and native fib(30) fell 23.820 -> 13.947 ms (-41.4%). Selection remains narrow and did not improve call_hot.
  • Generic activation-frame caching was measured, regressed, and deleted. See the rejection ledger below.

Mach

  • Residual type and text predicate branches are fused where their exact target and fact spans prove it safe.
  • Numeric null remains an absorbing poison in fast arithmetic and is canonicalized when observable.
  • Exact facts survive supported inline splices and are consumed by compact typed handlers.
  • Array read/store work described above materially reduced fannkuch dispatches.

Tooling retained because it made the evidence trustworthy

  • typed one-shot benchmark and dump commands with explicit reply deadlines;
  • paired optimizer statistics and fact/IL/assembly dumps;
  • independent golden benchmark results rather than Mach/native agreement alone;
  • deterministic fuzz progress without corrupting the final reply;
  • emitted IL in native cache keys;
  • bounded, identity-aware POSIX daemon lifecycle behavior;
  • native suspension, nursery barrier, record capacity, and daemon lifecycle native fixtures.

Performance evidence

The fresh campaign baseline at 81685c37 was:

benchmarkMach msnative ms
mandelbrot108.3028.01
fannkuch297.85241.13
spectralnorm305.97172.10
binarytrees97.6660.71
dynamic closure58.0730.43
Fibonacci54.2428.79
self TCO89.1330.30

Latest exact checkpoint evidence currently recorded:

benchmarklatest measured lanerecorded resultscope
mandelbrotnative9.21 msunchanged control at active structured checkpoint
spectralnormnative44.32-44.80 msactive structured raw-value lanes
spectralnormMach179.10 mspreceding inline-cleanup checkpoint; active QBE work does not change Mach
fannkuchnative106.135 msbounded raw-int loop-pressure checkpoint
fannkuchMach227.443 msfused checked-store checkpoint
binarytreesnative46.77 mscomposed record-template checkpoint
binarytreesMach86.42 mssame checkpoint; templates themselves are native-only
self TCOnative5.312 msstructured raw int/double TCO phis
Fibonaccinative13.947 msnarrow compact raw-recursion convention
record constructionnative14.04 mscontrolled templates on, 21.55 ms off

These figures establish large real movement, especially for scalar numeric programs, array-heavy fannkuch, recursion, and record construction. They are not a final release chart. Later commits were validated with focused controls, but at this historical checkpoint the entire branch had not yet been timed in one quiet fresh-process matrix. That matrix is now complete in perf-campaign-final-matrix.md.

The central unresolved dynamic rows remain:

  • generic dynamic closure calls and captured outer-frame lookup;
  • general record reads/writes and instance size;
  • unknown/mixed array element access;
  • allocation/GC under a validated nursery configuration.

Rejected or superseded experiments

The implementation for every rejected compiler/runtime arm below was deleted. Do not retry it without a materially different proof, representation, or cost model.

AttemptEvidenceDisposition
broad inlining and level-1 capture admissionspectralnorm native 172.10 -> 172.20 ms; IL +16.6%; allocation 2.6 -> 26 KiBreject size and allocation growth without speed
inline exact native array reads at every proven sitearray_read only -0.49%; spectralnorm not repeatable; IL/assembly grewretain shared helper; do not replicate the full control diamond
broad raw array add/sub propagationarray_read regressed about 31%keep only proven immediate multiply/divide arm
resolved-call direct native symbol entryno-inline call_hot 27.06 -> 28.67 ms (+6%)target identity alone is not the dominant call cost
prepared activation-frame cache31.37 -> 40.46 ms with eager scrub; 36.74 ms with scanned retentionbump/stamp/pop is cheaper; see perf-call-activation-cache.md
all eight fannkuch loop phisnative 105.97 -> 127.45 ms; IL smaller but assembly +6.7%QBE register/spill pressure defeats broad selection
add any raw-double/bool phi to fannkuch’s one bounded intevery tested addition slowed, one to 128.53 msretain bounded-int-only overflow policy
promote numeric parameters broadlyTCO improved but ordinary recursive Fibonacci materially regressedrequire an exact loop/call convention identity
Mach arithmetic plus cold-arm jump fusiondispatches fell 224.4M -> 192.4M, time 276.48 -> 277.30 msfewer dispatches alone are not enough
instruction-pattern cold block layoutself-realization stalled/failed; poisoned generated cacheretry only from verified CFG blocks
full typed Mach checked-store handlerall stores selected but time regressed 1-3% and text grewcurrent generic allocation-free store is better
exact transient spectral inline-chain recognizerreduced IL, but retry’s boundary oracle failed in Mach before a native timing claimdo not reconstruct pattern walker; active value identities are the replacement path
first record-heavy structured IR prototypeexact round trip, but roughly 24x retained graphs and sampled 512 MiB compiler heapreject storage implementation, not structured architecture
separate scalar native continuation stackcorrectness/memory model superseded by ordinary GC-walkable Pit frameskeep unified frame representation

Memory ledger

Target/runtime improvements

  • known-field record sizing: allocation traffic -44.4%, collections 62 -> 33 in binarytrees;
  • native AOT side-table initial allocations: roots 17,408 -> 544 bytes, key cache 1,024 -> 128 bytes, code cache 3,072 -> 384 bytes;
  • compact selected recursion: 48 fewer live frame bytes per private activation;
  • accepted native loop/value transforms generally reduce generated code and add no frame, heap, or per-code target metadata.

Target/runtime costs

  • native record templates: about 1.25 KiB shared runtime text plus 40-72 bytes per selected key-set descriptor; ordinary instance size is unchanged;
  • cooperative native execution: 64 bytes of native state per context and a lazy 136-byte four-entry host chunk, with no extra word per Pit frame;
  • compact raw recursion: about 596 bytes shared runtime text/unwind, offset by smaller live frames and slightly smaller selected target code;
  • Mach fusions add small shared handler text, generally hundreds of bytes to a few KiB each. A final linked-runtime size comparison is still required; individual reports record exact deltas.

Compiler-only costs

  • active structured spectral analysis increased QBE-emission allocation traffic 10.65 -> 13.07 MiB (+22.7%), allocations +45.3%, and sampled old-heap block peak 8 -> 16 MiB. Generated target memory did not increase and linked spectral text became 576 bytes smaller;
  • the discarded feasibility prototype retained about 24x raw mcode and reached a sampled 512 MiB compiler heap. Its representation is not present here;
  • the final integer-range state-product guard prevents very large self-hosted functions from exhausting the compiler heap while retaining all measured benchmark proofs.

Nursery status

The nursery implementation was repaired and instrumented, including the missing barrier and oversize pretenure behavior. It is still default-off. Current-tree performance acceptance is not established because supported daemon/test lifecycle failures blocked a matched off/on matrix. The current host policy starts with an 8 KiB nursery block and can grow to 512 KiB; neither is an accepted compact-target default. Do not quote historical nursery wins as current results. Resume with the explicit 1/2/4/8 KiB by 8/32/128/512 KiB sweep in perf-nursery-revisit.md.

Playdate nan32 status

The branch includes an accepted design and a standalone host/ARM compile-only fixture, not a 32-bit Pit runtime. Accepted semantics are raw finite binary32, signed-22-bit immediate integers with larger integers represented as float32, and one process-wide bounded runtime arena. Pointer/key representation and a real constrained-device arena remain implementation work. See pitvalue-playdate32.md.

Structured mid-end disposition

The first read-only feasibility prototype answered the semantic question: blocks, exceptional edges, dominance, loops, ordinary SSA values, captured cells, effects, origins, and exact mcode round trip can all be modeled. It was rejected as a production representation because record-per-node storage and iterative SSA used excessive compiler memory.

The current branch then implemented a narrower active view after finalized mcode and proved its executable value: spectralnorm native fell about 41% at that checkpoint with smaller generated code and no target memory. Therefore:

  • structured value identity is an accepted optimization lever;
  • the current active view is a conservative QBE consumer, not yet the sole compiler IR;
  • compile time is not a gate, but compiler memory is bounded to prevent failed self-host builds;
  • future expansion should use stable values/origins and explicit heap/cell effects rather than more instruction-pattern recognizers;
  • Mach must eventually consume the same semantic facts where useful.

Historical PGO and shapes plan (superseded by landed code)

The following subsections record the pre-implementation design. Dense shapes and the bounded PGO record-load consumer subsequently landed with the same core contracts; see the final update above and perf-linked-pgo-arc.md.

Static record shapes v1

The agreed first representation experiment is:

  1. a literal known key set gets one immutable shared layout descriptor;
  2. an instance stores dense values rather than repeated keys/hash slack;
  3. updating an existing field remains dense;
  4. adding a field or deleting one converts that record to the existing generic dictionary representation;
  5. no transition tree or unbounded hidden-class graph;
  6. monomorphic/polymorphic describes an access site, not the converted record.

This was expected to reduce construction, lookup, GC scanning/copying, and instance memory. It is now implemented and retained. A common three-field record is 56 rather than 144 bytes; the full tradeoff, including the final binarytrees native caveat, is in perf-record-shapes.md.

PGO spine

The agreed profile is semantic and keyed by stable origin plus exact executable content. Instrumented Mach realizations collect execution counts, branches, receiver/result kinds, record layout IDs, array kinds, callee code IDs, loop trips, and disruption frequency. Static facts remain truth; profile data is a hint requiring guards and a generic fallback.

Profiles can feed both Mach and native and can be collected on a development machine for cross-target AOT builds. Branch/type/callee frequencies are portable; target cost decisions are not. Running actors retain their code generation; future actor starts can use a newer optimized realization. Final target artifacts need not contain counters.

Primary consumers are:

  • record layout guard plus dense offset read/write;
  • dynamic closure code-identity guard plus a thinner freeze-safe call path;
  • array length/element-kind guards and loop versioning;
  • branch/cold layout, inlining, and specialization budgets.

PGO cannot make a hash record small or a general call convention cheap by itself. Shapes and the call convention are the fast representations PGO chooses.

LTO: existing seed and required extension

use() is already statically constrained and the builder already resolves a module graph, execution order, and static bindings. pit-compiler/lto.cm collects used module export members and contains a conservative export-pruning transform. The dependency graph aggregates importers, but production compilation does not currently invoke the pruning transform.

engine_lite still loads each script module in order and caches its stone actor-local return value. That runtime representation does not block LTO. A logical linked-program view can preserve unit IDs and module initialization while enabling:

  • known module-export offsets/bindings;
  • cross-module resolved callee sets and inlining;
  • unused export/function/module removal;
  • whole-program escape/effect analysis;
  • PGO-guided cross-module layout and specialization.

Do not begin by concatenating source or deleting module boundaries. Qualified (unit, function, value, origin) identities preserve caching, diagnostics, profile provenance, initialization order, and actor-local module values. One physical Mach/native blob can be evaluated later as a lowering/layout choice.

Endowments and native C calls remain opaque external effects. The set of Pit code bodies is closed for a fixed executable, but messages, mutable aggregates, selected closures, actor inputs, and C results remain honest dynamic data.

  1. Run one quiet, fresh-process final micro/shootout/call/memory matrix at this branch and record artifact hashes. Do not infer the final matrix by adding deltas from different checkpoints.
  2. Implement and falsify static dense record shapes v1. This supplies the memory/access representation needed by PGO and has a strong low-memory thesis even before feedback.
  3. Add stable origin IDs and the smallest complete PGO artifact loop: instrumented Mach -> profile -> guarded optimization -> ordinary Mach and native. Start with record layout IDs because correctness and payoff are easiest to measure.
  4. Design and measure a general thin freeze-safe call/return convention. Use PGO to guard closure code identity, retain the actual environment, and keep an explicit actor suspension boundary. Do not reuse prepared frames.
  5. Extend the current static module graph into logical LTO: export pruning, module-field offsets, resolved cross-unit callees, then cross-unit inlining and escape analysis.
  6. Use PGO for dynamic arrays: hoisted length/representation guards and loop versions first; consider typed backing stores only if boxed load/decode remains material.
  7. Re-run the nursery matrix after lifecycle tooling is reliable and after shapes reduce record traffic. Choose a low-memory Pareto point rather than the fastest host-only cap.

Historical validation state at handoff

The accepted experiment reports individually record full-suite, focused, dual-lane, suspension/GC, and deterministic fuzz gates. The latest active structured checkpoint records:

  • compiler tests 121/121 at that commit;
  • structured tests 7/7;
  • default suite 1,918/1,918 outside socket-restricted sandboxing;
  • deterministic fuzz 3,733/3,733;
  • forced native suspension with 48 moving collections.

After adding the integer-range state-product guard, the current branch passed 124/124 focused compiler tests, 7/7 structured tests, and 1,918/1,918 default tests. At this checkpoint a final performance matrix was pending; it has since completed and is recorded in perf-campaign-final-matrix.md.

Generated boot objects and OCaml .cmi/.cmx benchmark products are build artifacts, not performance source. They should not be committed merely because validation regenerated them.

Source: plans/archive/compiler-optimizing-handoff.md