Archive — history, not state. Kept for its reasoning and its evidence; its plan is closed.
Compiler switch report — what each switch changes on a real artifact
Measured 2026-08-05 (night of 2026-08-04) on branch cp/switch-report,
base commit 1d65dd7da10a640b17f8017623904795f212ae89 (lane/cp, “Merge
p/dedup-census into lane/cp”). Every number below comes from runs made
tonight in the worktree .claude/worktrees/night-switchreport; nothing is
quoted from plans/one-binary.md or plans/compiler-perf.md.
1. The artifact
The pit-shop/clerk linked closure — the P7 census corpus.
| property | value |
|---|---|
| entry | pit-shop/clerk.ce |
| modules in the linked closure | 99 |
| source lines | 59,305 |
| functions in final IR (ship) | 4,195 |
corpus identity (sha256 of the sorted path list + shasum -a 256 of each file) | 9acbaec8b5a40dac243c054e01a9b268bba567a9a316064a1fccd419fc6e22a8 |
| executable hash reported by the shop for the same closure | blake2:8b2366df4d031c570c484a051b718628004716f3bd547a0410e20195b84ea482 |
The 99 locators came from the shop itself:
./pit compile linked_plan pit-shop/clerk.ce # module_count: 99
and were mapped to the 99 source files listed in
plans/night-2026-08-04/corpus.txt (locator pkg/mod → pkg/mod.cm|.ce).
P7 recorded 4,188 functions against 4,195 here; the seven-function drift is
ordinary source movement between that census and this tip.
2. Method
The matrix is not run through the shop. A shop realization bakes one
compiler profile into the derivation key, so switching passes per row would
have measured cache behaviour rather than compiler behaviour. Instead a
single harness actor compiles all 99 sources itself, once per row, with the
switch record handed straight to streamline:
compiler.mcode_unit_result(src, path, {
link_mode: "pool",
streamline_options: {profile: <name>, stage: "unit", passes: {...}}
})
The harness is committed beside this report as
plans/night-2026-08-04/switchbench.ce. Reproduce a row with:
cd <worktree>
make # once
echo <config-name> > $SCRATCH/config.txt
cp plans/night-2026-08-04/corpus.txt $SCRATCH/paths.txt
./pit $SCRATCH/switchbench.ce # writes $SCRATCH/rows/<config>.json
(the harness has the scratch directory as a def SCR constant at the top —
point it at your own).
Column definitions, all measured on the final IR:
- instructions —
sum over every function of ir_stats.detailed_stats(fn).instracross all 99 units. Labels are excluded,_nop_rows are counted. This is not the linker’s ownprogram_code_statscounter; that one counts every row and reads ~1.34x higher (see §5). - guards — the same walk’s
.guardcount (the nine census guard ops). Equal, in every row, toguard_reasons.analyze’scensus_sites. - guard-reason top-5 —
shop_tools/internal/guard_reasons.cmsummary.census_by_reason, merged across all 99 units. - literal-pool rows — per-module rows:
sum length(unit.literals). Linked rows: the program’sstats.literal_rows_after. - artifact bytes —
shoplib::canonical.encode(binary Nota) of the portable mcode payload: per-module, the sum over the 99pit.mcode.unit@3records; linked, the sum over the 99 literal-stripped linked units plus the one shared literal group. This is the mcode artifact, not a pressed Mach pool and not a cart. (mach_pressrefuses a pool-lane unit without an import covering, so pool bytes were not available for a like-for-like row.) - compile s / link s —
time.monotonicinside the harness actor around the 99-unit compile loop and aroundmcode_link.link_resultseparately. Process startup, file reads and the census walk are outside both clocks.
Linked rows use P6’s semantic linker with the SHIP_PLAN shape from
shop_tools/internal/diff_runner.cm:
{schema: "pit.mcode.link-plan@1", profile: "ship",
granularity: "program", stripping: {names: false, logs: []}}
Import edges were wired by resolving each unit’s use() claim
package-qualified (pkg::mod → pkg/mod, bare mod → <own pkg>/mod)
against the 99-locator set. 238 of 373 distinct claims resolved; the
remaining 135 are C natives, platform alias modules and fallback-package
resolutions and were left unwired. §6 shows why that does not move the
result.
3. The matrix
Baseline for the “vs ship” columns is row 2. Total s = compile + link.
| # | row | instructions | vs ship | guards | vs ship | literal rows | artifact bytes | vs ship | compile s | link s | total s |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | dev (per-module) | 857,842 | +0.32% | 113,678 | +1.02% | 8,552 | 57,739,798 | +0.14% | 120.8 | — | 120.8 |
| 2 | ship (per-module) — the shipped form | 855,147 | — | 112,533 | — | 8,552 | 57,660,153 | — | 104.7 | — | 104.7 |
| 3 | small (per-module) | 854,998 | −0.02% | 112,533 | 0.00% | 8,552 | 57,649,072 | −0.02% | 95.7 | — | 95.7 |
| 4 | ship + join_types ON | 852,044 | −0.36% | 112,269 | −0.23% | 8,545 | 57,487,982 | −0.30% | 209.5 | — | 209.5 |
| 5a | ship − type_checks | 1,054,404 | +23.30% | 166,329 | +47.80% | 8,592 | 69,882,875 | +21.20% | 129.9 | — | 129.9 |
| 5b | ship − guardcse | 897,354 | +4.94% | 123,034 | +9.33% | 8,564 | 59,541,422 | +3.26% | 105.9 | — | 105.9 |
| 5c | ship − notstone | 856,676 | +0.18% | 112,673 | +0.12% | 8,552 | 57,782,150 | +0.21% | 121.6 | — | 121.6 |
| 5d | ship − inline | 854,998 | −0.02% | 112,533 | 0.00% | 8,552 | 57,649,072 | −0.02% | 111.3 | — | 111.3 |
| 5e | ship − panic_outline | 946,742 | +10.71% | 112,537 | 0.00% | 8,552 | 64,636,824 | +12.10% | 112.2 | — | 112.2 |
| 6 | ship FULLY LINKED (pit.mcode.program@1) | 841,997 | −1.54% | 111,025 | −1.34% | 3,544 | 54,271,643 | −5.88% | 104.2 | 67.8 | 172.0 |
| 7 | ship linked + join_types ON | 841,364 | −1.61% | 110,951 | −1.41% | 3,543 | 54,239,684 | −5.93% | 224.3 | 69.0 | 293.4 |
Rows 5a–5e turn exactly one switch off relative to row 2; everything else
in the ship profile stays on. Rows 6–7 compile every unit at
stage: "unit" first (that is the compile s column) and then run the
program linker over all 99 units at once (link s).
Guard-reason top-5
| # | row | top five census guard reasons |
|---|---|---|
| 1 | dev | slot_type_unknown_at_join 41,689 · dynamic_read_result 20,806 · param_polymorphic 16,657 · call_result_polymorphic 12,792 · predicate_result_observed 10,448 |
| 2 | ship | slot_type_unknown_at_join 41,670 · dynamic_read_result 20,779 · param_polymorphic 16,581 · call_result_polymorphic 12,786 · predicate_result_observed 10,452 |
| 3 | small | slot_type_unknown_at_join 41,666 · dynamic_read_result 20,779 · param_polymorphic 16,580 · call_result_polymorphic 12,792 · predicate_result_observed 10,448 |
| 4 | ship + join_types | slot_type_unknown_at_join 41,412 · dynamic_read_result 21,079 · param_polymorphic 16,391 · call_result_polymorphic 12,691 · predicate_result_observed 10,443 |
| 5a | ship − type_checks | slot_type_unknown_at_join 63,080 · type_unknown 26,401 · dynamic_read_result 22,505 · param_polymorphic 19,835 · call_result_polymorphic 14,700 |
| 5b | ship − guardcse | slot_type_unknown_at_join 43,480 · dynamic_read_result 22,266 · param_polymorphic 18,897 · call_result_polymorphic 13,876 · predicate_result_observed 10,455 |
| 5c | ship − notstone | slot_type_unknown_at_join 41,670 · dynamic_read_result 20,779 · param_polymorphic 16,589 · call_result_polymorphic 12,786 · predicate_result_observed 10,452 |
| 5d | ship − inline | slot_type_unknown_at_join 41,666 · dynamic_read_result 20,779 · param_polymorphic 16,580 · call_result_polymorphic 12,792 · predicate_result_observed 10,448 |
| 5e | ship − panic_outline | slot_type_unknown_at_join 41,688 · dynamic_read_result 20,902 · param_polymorphic 16,341 · call_result_polymorphic 12,879 · predicate_result_observed 10,452 |
| 6 | ship linked | slot_type_unknown_at_join 41,051 · dynamic_read_result 20,693 · param_polymorphic 16,137 · call_result_polymorphic 12,466 · predicate_result_observed 10,447 |
| 7 | ship linked + join_types | slot_type_unknown_at_join 40,864 · dynamic_read_result 20,917 · param_polymorphic 16,061 · call_result_polymorphic 12,452 · predicate_result_observed 10,439 |
slot_type_unknown_at_join is the top reason in every row, at 37.0% of
ship’s 112,533 census guards. Nothing in the matrix moves it more than
1.9% (row 7, −806 of 41,670), and turning type_checks off raises it by
51%.
Linker’s own counters (rows 6 and 7)
The linker reports its statistics over its own instruction walk, which counts every IR row. Recorded verbatim so this run is comparable to P7’s:
| stat | row 6 (ship linked) | row 7 (+ join_types) |
|---|---|---|
link_instructions_before | 1,141,278 | 1,138,284 |
link_instructions_after | 1,128,125 | 1,127,601 |
link_instructions_removed | 13,153 | 10,683 |
link_guards_before | 118,081 | 117,807 |
link_guards_after | 116,519 | 116,436 |
link_guards_removed | 1,562 | 1,371 |
literal_rows_before | 8,549 | 8,544 |
literal_rows_after | 3,544 | 3,543 |
literal_rows_coalesced | 5,005 | 5,001 |
link_group_count | 1 | 1 |
finalized_sites | 0 | 0 |
constant_members / function_members / generic_members | 0 / 0 / 1,334 | 0 / 0 / 1,334 |
link_fact_slots | 0 | 0 |
P7 recorded 1,122,900 instructions and 8,890→3,662 literal rows on this closure; tonight’s 1,128,125 and 8,549→3,544 are the same measurement on a slightly moved tree.
4. Findings
The three profiles are not three profiles. dev, ship and small produce
outputs within 0.32% of each other on instructions, 1.02% on guards and
0.14% on bytes. dev — which turns off inline, licm, int_ranges, sroa,
forward and switch — is worse than ship on every output axis and slower to
compile (120.8 s vs 104.7 s). Whatever dev is buying, it is not compile
time on this corpus; the passes it disables (forward, sroa) shrink the
IR that every later pass then has to walk, so removing them costs more than
they cost. small is exactly ship minus inline, and its output is
byte-identical to row 5d, which is how we know the output columns carry no
run-to-run noise at all.
Which switches pay:
| switch | instructions if OFF | bytes if OFF | verdict |
|---|---|---|---|
type_checks | +23.30% | +21.20% | the pass. Also pays for itself in compile time: turning it off made the compile 24% slower, because every later pass then walks 47.8% more guards. Nothing else is close. |
panic_outline | +10.71% | +12.10% | strong size win, guard-neutral. It moves 535 panic tails into shared helpers (4,195 → 3,660 functions when off, i.e. 535 helpers exist when on). Compile cost is inside the noise band. |
guardcse | +4.94% | +3.26% | free win. 10,501 guards removed (9.33%) for a compile-time delta of +1.1%, which is inside noise. |
notstone | +0.18% | +0.21% | marginal but positive and cheap. |
inline | −0.02% | −0.02% | buys nothing in size — the output is very slightly smaller with inlining off. Its case has to be made on runtime speed, which this matrix does not measure. It is the only difference between ship and small. |
join_types does not clear John’s bar, by a wide margin. Row 4 against
row 2: −0.36% instructions, −0.23% guards, −0.30% bytes, for 209.5 s
against 104.7 s — exactly 2.00x compile time. The standing ruling is that
~5% program win is worth 2x compile. This is 0.36% for 2.00x, roughly 14x
short. On top of the fully linked baseline (row 7 vs row 6) it is worse
still: −0.075% instructions and −0.06% bytes for +120 s. The default-off
setting in pit-compiler/passflags.cm is correct as it stands, and this
corpus does not support flipping it — even though slot_type_unknown_at_join
is the largest guard class, join-aware facts clear only 258 of its 41,670
sites (0.62%) at the unit stage.
What link time buys, and it is the best row in the table. Row 6 against row 2, holding the unit-stage compiler identical:
- literal-pool rows 8,552 → 3,544 (−58.6%, 5,005 rows coalesced into one program group)
- artifact bytes 57,660,153 → 54,271,643 (−5.88%, 3.39 MB)
- instructions −1.54% (13,150 rows), guards −1.34% (1,508)
- cost: +67.8 s on 104.7 s, i.e. 1.65x total compile time
That is a 5.9% artifact win for 1.65x — the only lever in this report that
clears the 5%-for-2x bar, and it clears it with room. Most of the byte win is
the shared literal pool (5,005 rows coalesced); the link-stage streamline
contributes the −1.54% instructions / −1.34% guards on top. The 67.8 s was
not broken down between the two phases. Link cost is also a once-per-closure
charge rather than a per-unit multiplier, so it does not tax the
edit-compile loop the way join_types does.
P6’s import-edge finalization fires on nothing here, and this is not a
wiring artifact. finalized_sites is 0 with 1,334 member references seen.
The reason is not the 135 unwired native/alias imports: running
mcode_link.export_summary directly over all 99 compiled units returns
generic for 99 of 99. Zero modules in the shipped clerk closure
present the shape the recognizer requires — exactly one return, of a
stone(record) built with no non-construction control flow in the module
body. Real modules branch while building their export record (platform
selection, capability probes, memoization), which sets control_safe false
and drops the candidate to generic. The size half of P6 is therefore
carried entirely by the literal pool; the direct-reference half is, on this
artifact, inert. If direct references are wanted, the recognizer has to be
taught the branch-and-freeze shape, not merely handed a better import graph.
Compile cost, in seconds, for the record. ship compiles the 99-unit
closure in ~105 s in-actor. Enabling join_types costs +105 s. Linking
costs +68 s. Disabling type_checks costs +25 s (a loss on both axes).
Everything else moves the clock by less than the measurement noise.
5. Caveats
- Wall-time is noisy; output metrics are exact. Two independent runs of
the identical configuration measured 104.70 s and 109.98 s for ship
(+5.0%), and rows 3 and 5d — semantically the same configuration —
measured 95.66 s and 111.27 s (+16.3%) while producing byte-identical
outputs. Treat any compile-time delta under about ±16% as noise. Only
join_types(+100%), linking (+65%) andtype_checks-off (+24%) are outside that band. All output columns (instructions, guards, literal rows, artifact bytes) are deterministic — rows 3 and 5d agree to the byte on all four. - One sample per row. No medians. Given caveat 1 this matters only for the time column.
- “Artifact bytes” is canonical mcode, not a cart. It is the binary
Nota encoding of the portable units (plus the program literal group when
linked). It is a faithful proxy for payload size and it is what the
literal pool acts on, but it is not
boot/root.cartbytes and it is not pressed Mach pool bytes.mach_presscannot press alink_mode: "pool"unit without an import covering (mach_pool_emit: pool-global VALUES link disrupted), so a pool-bytes column could not be made like-for-like across linked and unlinked rows. - Instruction counts here are lower than the linker’s. 855,147
(per-function
detailed_stats) vs the linker’s 1,141,278 for the same program; the linker counts every IR row including labels. Both numbers are reported so this run can be compared either way. - 135 of 373
use()claims were left unwired in the linked rows (C natives, platform alias modules, fallback-package resolutions). This is a floor on import finalization — but see §4: the export recognizer rejects all 99 units regardless, so a complete graph would not have raisedfinalized_sitesabove 0. - Row 7 turns
join_typeson at the unit stage only. The link plan’sprofilefield is validated againstdev|ship|bootonly (valid_link_planinpit-linker/mcode_link.cm), so there is no way to hand the link-stage streamline ajoin_types: trueoverride without editing the linker or adding a profile. Row 7 is therefore “unit-stage join facts, then the ordinary ship program link”. The link-stage half of join_types is unmeasured. - The switch surface is
passflags.resolveoptions, not the shop. No shop toggle was exercised; the derivation key still bakes onecompiler_profile. Nothing here says how these switches behave through a realpit build. - The tree’s known linker-test disruptions were not exercised.
tests/mcode_link.cm’s three disrupting tests were left alone as briefed. The linker itself did not disrupt in any row: both linked rows returned a validpit.mcode.program@1with zero diagnostics and all 99 units materialized. - Two shop-side oddities were hit while collecting the corpus and are
worth someone’s time, but are not part of this report’s claims: a
cache-hit
./pit compile linked_plan pit-shop/clerk.cehung past the 600 s terminal session cap afterload_cached_executable(the cold run of the same command completed in 97 s), and amake-spawnedpit forgeprocess was still holding.pit/pit.pidaftermakeexited 0, which made the next client refuse with “shop daemon has different build signature”.
6. Raw rows
Everything this report claims is in the directory beside it:
rows/<config>.json— the eleven matrix rows verbatim as the harness wrote them, each with the full guard-reason and guard-op histogram and (for linked rows) the linker’s own stats block.rows/_exportprobe.json— theexport_summarycensus ({"generic": 99}) and the repeat ship compile used for the noise estimate in §5.switchbench.ce— the harness that produced every row.exportprobe.ce— theexport_summary/ repeat-compile probe.corpus.txt— the 99 source paths of the clerk closure, in the order compiled.
Source: plans/archive/night-2026-08-04/switch-report.md