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

Compiler optimization campaign: final decisions and tradeoffs

Date: 2026-07-14

Branch: compiler_optimizing

Status: final review index for the contained experiment. Nothing here was pushed or merged upstream. Measurements are from the clean-daemon integrated artifact unless a row explicitly says it is an isolated trial.

Outcome

The campaign substantially improved generated programs without changing Pit’s language guarantees or adding a semantic mcode operation. From the fresh contained baseline to the final composed artifact, native changed as follows:

workloadbaselinefinalchange
mandelbrot28.01 ms9.21 ms-67.1%
spectralnorm172.10 ms44.55 ms-74.1%
fannkuch241.13 ms111.35 ms-53.8%
binarytrees60.71 ms50.06 ms-17.5%
closure30.43 ms26.24 ms-13.8%
Fibonacci28.79 ms13.61 ms-52.7%
self tail call30.30 ms4.98 ms-83.6%

Mach improved 17-45% on the four original shootouts and 12.4% on the dynamic closure row. Scalar native arithmetic is now broadly competitive with OCaml and .NET. The remaining large gaps are representation and dynamic-boundary costs: boxed arrays, dynamic calls/closures, record access misses, allocation, and GC. nbody at 150.79 ms native versus 1.58 ms OCaml is the clearest combined stress case.

Retained production mechanisms

Exact compiler facts and native numeric lowering

  • Constants are point/use-site facts, not function-global slot facts. This fixes slot-reuse wrong code while preserving constant division, literals, range analysis, and later exact consumers.
  • The active structured QBE value lane keeps narrowly proven doubles raw across safe finalized-mcode regions. It moved spectralnorm from about 76 to 44-45 ms native with 576 fewer linked text bytes and no target heap/frame cost. Compiler-only cost on spectralnorm was +2.41 MB allocation (+22.7%), about 20 versus 8 ms QBE emission, and an 8-to-16 MiB old-block peak. Compiler speed was not a campaign gate; generated target memory remains unchanged.
  • Native positive-power-of-two divide/idiv/mod/remainder specialization is retained. The final canary was 15.14 versus 15.43 ms and 1,522 versus 1,551 QBE IL lines; the exhaustive edge fixture emitted 77,267 versus 77,837 lines. It has no runtime metadata or heap cost.
  • Wide whole literals now distinguish language is_integer from the current signed-int32 immediate lane. This is correctness-only and adds no cost to ordinary programs.

Records and PGO

  • Dense static record shapes reuse the record header’s I bit and cap56; there is no new per-record word. A three-field record is 56 rather than 144 bytes. One immutable descriptor costs 24 + 8 * key_count bytes in shared CT memory. Existing-field writes stay dense; add/delete converts that record one-way to the unchanged generic representation.
  • Final record_new was 21.18/14.85 ms Mach/native with 16.817 MB allocation and 20/11 collections. The generic-template canary allocated 43.217 MB.
  • Dense shapes have one explicit caveat. In the final binarytrees canary they were 74.79/47.25 ms versus generic-template 83.63/44.86 (repeat 80.34/44.73) and no-shape 80.37/51.66. Thus dense is a large memory/Mach win and faster than no shapes, but about 5% slower native than generic-template. It remains retained because allocation falls from 53.96 to 43.25 MB, common records are 61% smaller, and no-shape native is worse. The native gap is follow-up work.
  • Program-scoped PGO is explicit (--pgo_collect, --pgo_auto), portable, and keyed to exact executable semantic identity. It collects record-load sites only, caps memory/sites, and consumes only sites with at least 1,024 samples and 90% valid top-four shape coverage.
  • Final record-load PGO moved 53.875/30.147 to 45.954/22.895 ms Mach/native; collection was 56.283 ms. The profile is 7,088 bytes. The first consumed 64-bit Mach PIC costs about 144 bytes (function wrapper, code metadata, site row); later sites are about 64 bytes. Records gain zero bytes and existing shape descriptors are reused.

Calls and modules

  • Exact static use exports are cached in actor-local frame slots. The direct fixture improved Mach 16.81 to 13.90 ms and native 10.87 to 9.14 ms. The cost is one 8-byte live top-frame slot per cached export; module identity and dynamic export behavior remain intact.
  • Proven synchronous C leaves bypass a temporary Mach activation while retaining the C-module non-suspending contract. Generic and cached paths improved about 28-34% in the original gate. Runtime text grew about 992 bytes; there is no per-call heap object.
  • The fused Mach outer-frame cache combines the first depth-1 captured access with binding. Final canary: 51.42 versus 56.18 ms (-8.5%). It costs one scanned 8-byte slot per live eligible activation, zero serialized bytecode bytes, and preserves GC, frozen stacks, disruption, recursion, depth-2, and the captured write barrier. Native is unchanged.
  • Calls still use ordinary freezeable Pit frames. No optimization may hide a live continuation in an unscanned native or C-only side structure.

Artifact and tooling contracts

  • Semantic mcode is fact-free. A strict pit.mcode.lowering@1 sidecar carries finalized target facts and hydrates a private lowering view. Canonical units are pit.mcode.unit@2; cache identity is mcode-unit-v4-strict-lowering. Missing, malformed, unknown, mismatched, or inline facts fail closed; no compatibility fallback remains.
  • Final artifacts contain 65 strictly validated mcode modules: boot.qop 74,842,189 bytes, boot/firmware 67,509,488, and boot/boot 1,573,945. Combined size fell 1,095,242 bytes versus the immediate pre-audit campaign artifacts; the engine grew 126,372 bytes for strict validation/hydration. Their final SHA-256 identities are respectively a58afba087c65d9d04b31c10ade9c16400fc3766b3a3f8a8295749462eeb7d57, 797db28fc9b1bec61db89da36a5656049db9da3389ce72a6f0dc61c12b175e89, and 6fff594fcb6f130c9c2ab5db5c1f595081320ec56d1c7f6f11ea1ebb4bf17174.
  • Bare CLI source execution is typed one-shot request/response; the implicit two-second help probe is gone. PGO flags are program-scoped. GC benchmark detail is opt-in. These are workflow correctness improvements, not language speed claims.

Rejected and deleted experiments

experimentmeasured resultdecision
activation-frame cache31.37 ms baseline; 40.46 eager scrub; 36.74 stale-scannedbookkeeping exceeded bump/pop; delete
PGO check-only dynamic call58.768 to 59.241 mspredicate/arity checks are not the call bottleneck; delete consumer
fused native call deliverycall0 -0.9%, closure flat, call_hot +4.2%failed controls; delete
local array bounds proofMach -2.5%, native +2.1% noisechecks were not dominant; delete
sealed boxed-int array raw load0.09%boxing/representation still dominated; delete
PGO record-store PICMach -2.8%, native +0.2%ordinary shaped store already cheap; delete
exact linked constant-leaf substitutionMach about 13% faster; native 0.93% faster, below 25%/50% gatesdid not remove enough activation work; fully delete
eager closure outer bind57.71 to 65.00 msone added dispatch per call; delete
native closure outer-base CSEbelow 5%, noise-sizeddelete metadata/code
default nurserysome allocation rows improved, native neutral/slightly worse, per-actor reserve costkeep mechanism opt-in; default remains off
record-per-node structured IRabout 24x retained graph, sampled 512 MiB compiler heapreject representation, retain structured-value thesis
linked semantic clone/materialization armsdisruption/provenance failures or no executable gatedelete production integration

The PGO dynamic-call collector was also deleted because it had no surviving consumer yet could consume all 512 profile sites and roughly 32.7 KiB of call descriptors plus about 24.5 KiB of collector spine/counters. The profile schema remains extensible; there is no dormant call collection fallback.

Research-only code deliberately preserved

  • compact_spine.cm and its probe/tests are an isolated packed value/origin/effect representation. It retains roughly 0.91x raw mcode and is not imported by streamline, Mach, QBE, the linker, or runtime.
  • cross_unit_inline_experiment.cm is likewise a research experiment, not a production pass. Failed pairers, linked-call sidecars, pure-leaf integration, and executable fixtures were deleted.
  • The active structured QBE lane is production code; the earlier record-heavy structured representation is not.

Memory interpretation

The final benchmark actor numbers above are meaningful only on a restarted node. Clean boot mapped 64 MiB with 4 MiB actor heaps, 1 MiB node constants, about 20 MiB shared Mach, a roughly 2/8 MiB shop, and a 59/256 KiB builder. After running the aggregate and full compiler/test fleet without restart, the development daemon reached 704 MiB mapped, 278 MiB actor heaps, 37 MiB node constants, and a 242/256 MiB builder. Allocation rows then slowed by 2-6x; restart restored them. This is development compiler/cache retention, not the generated program’s target footprint, but it is a real tooling problem for repeatable benchmarking.

The nursery was off in the final matrix. Dense shapes are the campaign’s main runtime-memory win. PGO and closure caching add bounded bytes only where used; numeric/compiler proofs generally add no target memory. A future 32-bit PitValue/Playdate build remains separate work and was not produced here.

Final validation

  • make seed: passed.
  • Meson: 7/7; focused compiler: 145/145.
  • Isolated VM: 1,086/1,086; warmed full suite: 1,921/1,921.
  • Fuzz: 3,733/3,733, 500 programs, seed 20260713.
  • Aggregate: 68/68 exact Mach/native results.
  • One cold full-suite attempt passed 835 tests then charged realization to a vm_suite reply timeout; isolated and warmed reruns passed. It is not counted as a language failure or silently omitted.

Best next work

  1. Attack boxed-array representation, not another bounds-check proof. Static or profiled element kinds need a persistent specialized backing store before array_read, fannkuch, spectralnorm, or nbody can approach native peers.
  2. Redesign dynamic call/return work around freezeable ordinary frames. The rejected frame cache and check-only PIC show that activation/continuation delivery, not predicates, is the prize.
  3. Explain and remove the dense-shape native binarytrees gap while preserving the 56-byte record. Generated QBE/assembly and allocation/GC interaction are the first evidence to inspect.
  4. Extend the semantic linked graph only when a consumer removes a large cost. Constant-leaf substitution was too small; direct known Pit-function entry or a larger cross-unit inline must include activation/return and provenance.
  5. Keep PGO conservative. The proven record-load PIC is useful; store-only PIC and call-check PIC are not. New consumers need honest polymorphism, bounded metadata, and predeclared Mach/native gates.

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