08 β Bootstrap
Where we are
Chapter 07 closes at a824a175 ("Check for stack overflow."), 2023-07-18
15:04:09 β the compiler's own source has been squeezed, module by module,
into the narrow dialect it can read itself: no type classes, no
deriving, no fixity declarations, a hand-rolled Map, Int in place of
Integer. That last commit replaces the C runtime's fixed-size
interpreter stack array with a malloc'd one, sized by a new -K flag.
Three minutes and twenty seconds later, 456ce17c lands. The ladder
those two commits and everything before them were built for β GHC
compiles the compiler, the compiler compiles itself, the C runtime runs
that self-compile, and a byte-for-byte cmp checks the two results agree
β is aux/bootstrapping-a-compiler.md's
subject in full; read it first. This chapter narrates what happens the
moment that ladder gets make targets of its own and someone actually
climbs it.
What happened
The range a824a175..298c9824 is 28 commits, all by Lennart Augustsson,
spread across two and a half calendar days: nineteen commits on 07-18
(15:07β23:19), four on 07-19 (13:11β19:29), and five on 07-20
(11:20β14:58).
456ce17c: the milestone
456ce17c ("Add bootstrap test."), 15:07:29, adds one new Makefile
target β four lines (456ce17c:Makefile lines 66β69, quoted whole):
bootcombtest: $(BIN)/uhs uhs.comb
$(BIN)/uhs -ilib -isrc -omain-uhs.comb MicroHs.Main
$(BIN)/eval -H10000000 -K1000000 -ruhs.comb -- -ilib -isrc -omain-comb.comb MicroHs.Main
cmp main-uhs.comb main-comb.comb
Line one has the GHC-built bin/uhs compile MicroHs.Main β its own
compiler's entry point, the whole src/MicroHs/lib tree it imports β
straight to main-uhs.comb. Line two hands uhs.comb (a separate,
already-built target: bin/uhs compiling itself to a combinator file) to
bin/eval, the C runtime, and tells it to run that combinator file as a
program, with the same compiler arguments (-ilib -isrc -omain-comb.comb MicroHs.Main) tacked on after the --. uhs.comb is uhs, translated
into the combinator terms eval already knows how to run β chapter
02's .comb format, unchanged in shape since then. So line two is the
interpreted compiler compiling the same source a second time, writing
main-comb.comb. Line three is the oracle: plain byte equality, no
special-casing. If a native, GHC-built compiler and that same compiler,
translated into combinators and run by the C interpreter, produce
identical output compiling their own source, the ladder holds. The full
treatment of why cmp (rather than some semantic check) is the right
oracle, and of bootboottest, the parallel "local lib" rung, is the aux
doc's; this chapter's job is watching bootcombtest actually run.
fc7a2ad7 / f06bacb7 / 1876f4c1: the reality of a compiler-compiling-compiler workload
Running a compiler on nothing but a few short test files, the way every
prior chapter's Try-it does, and running it on itself β 30 .hs files
under src/ and lib/, 73,856 bytes combined at this commit (counted
with git ls-tree -r -l 456ce17c -- src lib) β are different workloads, and the
difference shows up within the hour.
fc7a2ad7 ("Add comments."), 15:36:42, is mostly what its title says:
short /* ... */ annotations next to eval.c's combinator cases (/* K x y = * x */ and the like). But three of its added lines, sitting right
above the IO_BIND case, are not decoration β they're a TODO note
written against a real, anticipated problem (fc7a2ad7:src/runtime/eval.c
lines 977β979, quoted whole):
/* XXX could use associativity to avoid deep evalio recursion. */
/* (m >>= g) >>= h === m >>= (\ x -> g x >>= h) */
/* BIND (BIND m g) h === BIND m (\ x -> BIND (g x) h) == BIND m (C' BIND g h)*/
Thirty-five minutes later, f06bacb7 ("Commented out non-working
alternative."), 16:12:11, tries a fix β in the library, not the
runtime. lib/System/IO.hs's hGetContents reads a file handle one
character at a time, and its recursive case is an ordinary do-bound
>>=: each character read nests one more IO.>>= around the rest of the
file (f06bacb7:lib/System/IO.hs lines 126β135, quoted whole):
-- Strict hGetContents
hGetContents :: Handle -> IO String
hGetContents h = do
c <- P.primHGetChar h
case c == negate 1 of
False ->
do { cs <- hGetContents h; return (chr c:cs) }
-- This should use less stack, but it doesn't work. :(
--return (chr c : P.primPerformIO (hGetContents h))
True -> return ""
This is exactly the site the TODO comment above was worried about:
src/MicroHs/Compile.hs's readFilePath calls IO.hGetContents to read
every module the compiler compiles, so a source file of N characters
builds a chain of N nested >>=s before any of them run. The commented-
out line tries routing around that with P.primPerformIO instead of a
recursive do-bind; the comment says plainly it doesn't work, and the
attempt is left in place, dead, rather than deleted.
Twenty-one seconds after that, 1876f4c1 ("Use associativity of IO.>>=
to avoid deep evalio recursion."), 16:12:32, fixes the actual mechanism β
in the runtime, where the TODO comment said it belonged. Before this
commit, evalio's IO_BIND case handles m >>= g by calling evalio
recursively on m β so for a left-nested chain (m >>= g) >>= h,
evaluating the outer bind means a nested C function call to evaluate the
inner one, one native stack frame per bind in the chain. A hand-read of
one character produces one bind; a source file produces as many nested
binds as it has characters, and that recursion runs on the C process's
own call stack, not the interpreter's own -K-sized NODEPTR stack
a824a175 just made growable β a different stack, with no such flag.
1876f4c1's fix uses the algebraic law fc7a2ad7's TODO comment already
named β (m >>= g) >>= h === m >>= (\x -> g x >>= h) β to rewrite the term
instead of recursing into it, and loops back to the top of the evaluator
via goto top rather than making a nested call
(1876f4c1:src/runtime/eval.c lines 980β998, quoted whole):
case IO_BIND:
CHECKIO(2);
{
/* Use associativity to avoid deep evalio recursion. */
/* (m >>= g) >>= h === m >>= (\ x -> g x >>= h) */
/* BIND ((BIND m) g) h === BIND m (\ x -> BIND (g x) h) == (BIND m) (((C' BIND) g) h)*/
NODEPTR bm;
NODEPTR bmg = evali(ARG(TOP(1)));
GCCHECKSAVE(bmg, 4);
if (TAG(bmg) == AP && TAG(bm = indir(FUN(bmg))) == AP && TAG(indir(FUN(bm))) == IO_BIND) {
NODEPTR g = ARG(bmg);
NODEPTR h = ARG(TOP(2));
n = new_ap(bm, new_ap(new_ap(new_ap(combCC, combIOBIND), g), h));
POP(3);
goto top;
}
}
x = evalio(ARG(TOP(1))); /* first argument, unwrapped */
Precision matters here: this is not a report of >>= producing wrong
answers, and nothing about it changes what any bind computes β every
m >>= g still ends up running g's continuation with m's result, in
order. What changes is how many native stack frames evaluating a
left-nested chain of binds costs: unboundedly many before this commit
(one per bind, however deep the chain), a constant number after it,
because the interpreter loop now iterates instead of recursing whenever
it can. The bug this closes is a stack-depth risk under a
character-at-a-time file reader run against real source files β a
compiler compiling itself is the first workload in this project's history
that produces IO chains long enough to make that risk concrete.
5d58461f: bigger ints, honestly
Six minutes and forty-eight seconds after the associativity fix,
5d58461f ("Use bigger ints."), 16:19:20, is not about the Haskell Int
type user programs see β that had already narrowed to machine width back
in chapter 06/07's diet. It widens the runtime's own bookkeeping counters
from int to int64_t: num_reductions, num_gc, num_marked,
max_num_marked, start_size, and several heap-indexing loop variables
in init_nodes, scan, gc, and parse_top
(5d58461f:src/runtime/eval.c lines 121β122, quoted):
int64_t num_reductions = 0;
int64_t num_gc = 0;
A test program's reduction and GC counts fit comfortably in 32 bits; a compiler compiling several thousand lines of its own source does not stay comfortably inside that margin for long. Nothing user-visible about program semantics changes β only how far the runtime's own statistics can count before silently wrapping.
1a84b483: give up when the heap is down to 2%
6ca78e8a ("Cleanup"), 23:16:14, and eight seconds later 1a84b483
("Give up when heap is down to 2%"), 23:16:22, land at the end of the
same long evening. The GC's gc() function already tracked how many
cells survived each mark-and-sweep pass; this commit adds one check right
after that count is known (1a84b483:src/runtime/eval.c lines 334β338,
quoted whole):
if (num_marked > max_num_marked)
max_num_marked = num_marked;
int64_t free = heap_size - heap_start - num_marked;
if (free < heap_size / 50)
ERR("heap exhausted");
heap_size / 50 is exactly 2% of the heap. This is not a smarter
collector or a bigger one β the heap size is still whatever -H says it
is β it's an honest bail-out: once a GC pass leaves fewer than 2% of the
heap free, the runtime stops and reports "heap exhausted" instead of
continuing to collect ever more often for ever less benefit, or running
out mid-allocation with no diagnostic at all. It is the runtime meeting
its own limit, for the first time, under a workload big enough to find
it β an honest ceiling stated in the same commit that discovers it
matters.
ee853660 / 04a4bbe4 / 8ad7094c: performance work, only after a real workload
Two days later, on 07-20, ee853660 ("Specialize the Map to
StringMap."), 11:20:05, deletes src/MicroHs/Map.hs β the general
Map k v chapter 07 built with an explicit equality argument on every
call β and replaces every use of it in Compile.hs, Desugar.hs,
Main.hs, and Translate.hs with src/MicroHs/StringMap.hs, a version
specialized to String keys with eqString baked in rather than passed
at each call
site: ee853660:src/MicroHs/Compile.hs line 79 reads case M.lookup nm ch of where the previous commit's same line read case M.lookup eqIdent nm ch of. Twenty-five minutes later,
04a4bbe4 ("Sort in frequency order."), 11:45:47, reorders two lookup
tables that are both searched linearly, string by string, on every
combinator or primop name the compiler ever reads or writes: eval.c's
primops[], which parse() scans on every .comb-file token, and
Translate.hs's primTable. Both diffs add a comment naming the same
reasoning (04a4bbe4:src/runtime/eval.c lines 169β190 and
04a4bbe4:src/MicroHs/Translate.hs lines 36β51, quoted whole, ...
marking the primop entries omitted after each combinator table):
/* We use linear search in this, because almost all lookups
* are among the combinators.
*/
struct {
char *name;
enum node_tag tag;
NODEPTR node;
} primops[] = {
/* combinators */
/* sorted by frequency in a typical program */
{ "B", B },
{ "O", O },
{ "K", K },
{ "C'", CC },
{ "C", C },
{ "T", T },
{ "S'", SS },
{ "P", P },
{ "I", I },
{ "S", S },
{ "Y", Y },
{ "B'", BB },
...
-- Use linear search in this table.
-- 99% of the hits are among the combinators.
primTable :: [(String, Any)]
primTable = [
("B", primitive "B"),
("O", primitive "O"),
("K", primitive "K"),
("C'", primitive "C'"),
("C", primitive "C"),
("T", primitive "T"),
("S'", primitive "S'"),
("P", primitive "P"),
("I", primitive "I"),
("S", primitive "S"),
("Y", primitive "Y"),
("B'", primitive "B'"),
...
Both tables move B and O to the front and push S, Y, and B' β the
combinators chapter 01's bracket abstraction produces least often in
practice β toward the back, on the strength of a one-line comment about
typical frequency, not a cited measurement. Between these two, at
16:50:16 on 07-18 (the same evening as the recursion fix, not the same
day as the reordering), 8ad7094c ("Add profiling option.") adds a
PROF variable to the Makefile, wired into bootuhs's build and link
steps: GHC's own -prof -fprof-auto cost-center profiler, applied to the
"local lib" GHC build of the compiler. Built and run this chapter's own
Try-it below, that profiler's report is dominated by
Text.ParserComb.(>>=), Data.List.(++), and Text.String.eqString β
Haskell-level cost centers inside the compiler's own parser and
library, measured on bootuhs, a native GHC binary. It has nothing to
say about which combinator names β S, K, B, O β turn up most often
in a .comb file, which is what 04a4bbe4's tables are sorted by; that's
a property of the C runtime eval and the translator Translate.hs,
neither of which the GHC profiler ever touches. Checked directly rather
than assumed: no commit message, code comment, or file in this range
connects the two. They read as two independent performance
interventions from the same 48-hour window β one instruments the
compiler's own Haskell, the other hand-tunes a linear search in a
completely different program β not a pipeline where one feeds the other.
db418344: type parsing, parse-only
db418344 ("Add type parsing."), 18:46:43, gives Parse.hs a real type
syntax for the first time since chapter 06 flattened type Type = Expr.
A new EType distinguishes type variables, constructor applications, and
unification variables, and ETypeScheme adds forall
(db418344:src/MicroHs/Parse.hs lines 99β107, quoted whole):
-- No higher order type variables (yet)
data EType
= TVar Ident
| TConstr Ident [EType]
| TUVar Int
--Xderiving (Show)
data ETypeScheme = ETypeScheme [Ident] EType
--Xderiving (Show)
Sign :: Ident -> ETypeScheme -> EDef now carries a real parsed type
scheme instead of an untyped Expr alias. But Desugar.hs, unchanged in
this respect by this commit, still turns every Sign into nothing
(db418344:src/MicroHs/Desugar.hs line 151, quoted): Sign _ _ -> [].
Types are syntax the parser understands and the EDef AST can carry, and
nothing downstream reads them yet β no checker, no inference, not even a
warning. The same parse-only posture chapter 03 gave the compiler's
earliest type signatures, one step further along; what those parsed types
eventually get used for is chapter 09's subject, not this one's.
298c9824: uhs becomes mhs β what actually renames
The range's last commit, 298c9824 ("Rename uhs to mhs."), 14:58:46,
touches exactly two files: Makefile and tests/Makefile. No file is
git mv'd, no module is renamed β MicroHs.Main was already called
that. What changes is a new MHS=mhs Makefile variable substituted
everywhere the literal string uhs had named a build artifact: the
binary ($(BIN)/uhs becomes $(BIN)/$(MHS)), its GHC-built,
local-library sibling (bootuhs becomes boot$(MHS)), the self-compiled
combinator file (uhs.comb becomes $(MHS).comb), the phony test target
(uhstest becomes $(MHS)test), and, in tests/Makefile, the UHS
variable itself is renamed to MHS=../bin/mhs. The project earns its
name β MicroHs β in its build vocabulary first. It doesn't earn it
everywhere at once: a grep -rn uhs over src//lib//ghc/ at this
commit still turns up one hit, src/MicroHs/Main.hs's own usage message,
unchanged by this commit (298c9824:src/MicroHs/Main.hs line 23,
quoted): error "Usage: uhs [-v] [-r] [-iPATH] [-oFILE] ModuleName". The
compiler renamed by this commit still calls itself uhs to its own
users' faces.
Undisclosed extras
Two commits carry more than their titles say. 787d8842 ("Add an
(unused) Data.Map"), 14:48:47, does add the promised file β 177 lines,
a real balanced 2-3 tree implementation, built but wired into no call
site (the Makefile only compiles it as part of bootuhs, never imports
it from anywhere that matters) β but the same commit also adds a
leIdent helper to Parse.hs and moves StringMap.hs's type
signatures into a new trailing block alongside a fresh commented-out
Data.Map-based alternative implementation, neither mentioned in the
title. 8e2f7109 ("Change test targets."), 16:25:51 on 07-18, mostly
does what it says β renaming and adding alltest/everytest targets β
but its bootcombtest diff also silently drops -K1000000 from the
eval invocation 456ce17c had added 1 hour 18 minutes earlier
(8e2f7109:Makefile line 67, quoted): $(BIN)/eval -H10000000 -ruhs.comb -- -ilib -isrc -omain-comb.comb MicroHs.Main β no -K1000000, and no
mention of it in the commit title β leaving bootcombtest
to run on eval's built-in default of 10,000 interpreter-stack cells
from then on β a hundredfold cut in an explicit safety margin, undisclosed.
On the other side of the ledger: acf40ed4 ("Remove junk"), 13:11:39 on
07-19, deletes A.hs and Test.hs β the exact two files 59b8b1ab
added, undisclosed, under the unrelated title "Fix name" back in chapter
07's range. Two weeks later, the same project removes them under a title
that says plainly what the diff does.
Why it matters
456ce17c isn't a feature landing; it's a fixpoint closing. Once
bootcombtest exists and passes, the compiler's own speed stops being a
side concern and becomes the benchmark that matters most: every commit
that makes the runtime or the compiler faster makes the next bootstrap
run faster too, and the bootstrap run is now the heaviest, most
representative workload in the whole project's test suite. That's
visible in the commit order itself. Every performance intervention in
this chapter β the int64_t widening, the 2%-heap bailout, StringMap,
the frequency-sorted lookup tables β comes after 456ce17c, several of
them within hours, none of them before. The runtime had gone 54 days,
from this repository's first commit (ccb31388, 2023-05-25) to
a824a175, without a single commit tuning its counters, its GC
threshold, its data structures, or its lookup order β earlier commits
titled "optimiser" (ae2a1403, d2956ee8, and others) are a different
concern entirely, code-quality passes over Exp.hs's bracket-abstraction
output, aux/bracket-abstraction.md's
subject, not runtime engineering. The self-compile is what finally gave
the runtime a workload worth engineering for, and the project engineered
for it immediately once it existed, not before.
Notebook margin
- The three fixes most directly forced by running a real self-compile β
1876f4c1's associativity rewrite,5d58461f's wider counters,1a84b483's heap bailout β land 7 hours 3 minutes 50 seconds apart at the outside (1876f4c1to1a84b483), and the tightest pair (f06bacb7to1876f4c1) is 21 seconds. - The StringMap/frequency-order/profiling cluster (
8ad7094c,ee853660,04a4bbe4) spans 42 hours 55 minutes 31 seconds end to end β still inside two days of456ce17c, even though the last two land on a different calendar day than the first. d8d8856b("Add version string to compiler binary."), 11:53:35 on 07-20, gives every.combfile a five-byte header ("v1.0\n") and adds acheckversioncheck toeval'smainthat reads and matches those bytes before parsing the rest of the file, oneERR("version mismatch")away from running a.combfile built by an incompatible compiler version.787d8842("Add an (unused) Data.Map"), the same afternoon, is the honest kind of speculative code this project allows: built, compiled, never called, and its title says so.
Try it
Anchor: 456ce17c ("Add bootstrap test."), 2023-07-18 15:07:29 β the
commit that adds the bootcombtest target this exercise runs. Convention
(see 00-overview.md):
git worktree add /tmp/mhs-456ce17c 456ce17c
cd /tmp/mhs-456ce17c
Build both binaries the target needs, exactly as written, no patch needed on this project's flake toolchain (GHC 9.8.4):
mkdir -p bin
nix develop $MICROHS -c make bin/uhs
nix develop $MICROHS -c make bin/eval
[...]
[12 of 12] Linking bin/uhs
gcc -Wall -O3 src/runtime/eval.c -o bin/eval
(The same -Wx-partial warnings on head/tail every earlier chapter's
Build notes already flag appear during the bin/uhs build; omitted.)
Now the money shot β the whole ladder, run as bootcombtest itself runs
it, timed step by step:
nix develop $MICROHS -c bash -c 'time bin/uhs -ilib -isrc -ouhs.comb MicroHs.Main'
real 0m0.964s
nix develop $MICROHS -c bash -c 'time bin/uhs -ilib -isrc -omain-uhs.comb MicroHs.Main'
real 0m0.942s
nix develop $MICROHS -c bash -c 'time bin/eval -H10000000 -K1000000 -ruhs.comb -- -ilib -isrc -omain-comb.comb MicroHs.Main'
real 0m21.273s
nix develop $MICROHS -c cmp main-uhs.comb main-comb.comb
cmp prints nothing and exits 0: main-uhs.comb and main-comb.comb β
68,799 bytes each β are byte-identical. The GHC-built bin/uhs compiled
its own source; that same source, translated into combinators and
interpreted by bin/eval, compiled it again; both runs produced the
exact same output. That's bootcombtest, in full, actually running,
at the commit that adds it β no stack overflow, no patch. (I also
re-ran the whole ladder at this range's last commit, 298c9824, with
bin/mhs in place of bin/uhs: main-mhs.comb and main-comb.comb
agreed there too, 75,052 bytes each, in 20.715s for the interpreted step
β so whatever native-stack risk 1876f4c1 closed, it isn't one this
environment's default 8 MB ulimit -s happens to trigger for this
particular self-compile; the fix is real and the mechanism above is read
directly from the diff, but I did not manage to reproduce the crash it
prevents.)
Exercise 1: time the eval-run self-compile against the GHC-built one
The three time runs above already answer this, on this machine: the
GHC-built bin/uhs compiles MicroHs.Main in well under a second
(0.942sβ0.964s, run to run); bin/eval interpreting the combinator
translation of that same compiler, doing the identical compile, takes
21.273s β a 22.6Γ slowdown (21.273 Γ· 0.942): native code versus a
graph-reduction interpreter walking the same combinator terms, on this
compiler's own source for the first time rather than on a short test
program the way every earlier chapter's time target measures it. The
same three-step timing at 298c9824, two days and 27
commits of source growth later (main-uhs.comb 68,799 bytes here versus
main-mhs.comb 75,052 bytes there, roughly 9% more program), gives
0.926s / 0.890s / 20.715s β a slightly better throughput per byte
(75,052 Γ· 20.715 β 3,624 B/s interpreted, versus 68,799 Γ· 21.273 β 3,234
B/s here), consistent with β though this single before/after pair can't
isolate exactly which of this range's performance commits earns it β
ee853660/04a4bbe4's lookup-table work paying for itself rather than
being pure overhead.
Exercise 2: corrupt one byte of uhs.comb
uhs.comb's text format names every combinator with a $-prefixed
token; find one and flip a single byte:
(python3 isn't in the devShell's buildInputs either β like diff in
chapter 01, it's whatever the host system provides on the PATH nix develop hands you.)
nix develop $MICROHS -c python3 -c "
data = bytearray(open('uhs.comb','rb').read())
assert data[53:54] == b'C'
data[53] = ord('Z')
open('uhs-corrupt.comb','wb').write(bytes(data))
"
Byte offset 53 (0-indexed) sits inside a \$C token near the start of
the file; the edit above turns it into \$Z , a combinator name that
does not exist. Ask eval to run it, the same way bootcombtest's
middle line does:
nix develop $MICROHS -c bin/eval -H10000000 -K1000000 -ruhs-corrupt.comb -- -ilib -isrc -ocorrupt-out.comb MicroHs.Main
eval: bad primop Z
ERR: no primop
Exit code 1. parse()'s primop table lookup (the same linear search
04a4bbe4 later reorders) finds no entry named Z, prints exactly the
diagnostic its fprintf/ERR calls promise, and the whole run aborts
before a single reduction happens β one corrupted byte, caught at parse
time, not miscompiled silently into the next bootstrap.
Exercise 3: the profiling flag and the frequency ordering β checked, not assumed
8ad7094c's PROF flag doesn't exist yet at the 456ce17c anchor, and
neither does 04a4bbe4's reordered primops[] table; this exercise
needs both, so it uses a second worktree at the commit where they last
appear together, 04a4bbe4 itself:
git worktree add /tmp/mhs-04a4bbe4 04a4bbe4
cd /tmp/mhs-04a4bbe4
mkdir -p bin
nix develop $MICROHS -c make PROF="-prof -fprof-auto" bin/bootuhs
[...]
ghc -prof -fprof-auto -hide-all-packages -o bin/bootuhs ghc-boot/*.o ghc-boot/*/*.o
nix develop $MICROHS -c bin/bootuhs -ilib -isrc -omain-boot.comb +RTS -p -RTS MicroHs.Main
This writes bootuhs.prof, GHC's own cost-center report (trimmed to the
top entries):
COST CENTRE MODULE SRC %time %alloc
>>=.\.xss Text.ParserComb src/Text/ParserComb.hs:80:15-69 18.8 7.4
++ Data.List lib/Data/List.hs:(22,1)-(25,26) 10.1 23.9
>>=.\ Text.ParserComb src/Text/ParserComb.hs:(78,5)-(82,68) 9.9 17.0
eqString Text.String lib/Text/String.hs:(101,1)-(110,44) 8.7 12.9
runP Text.ParserComb src/Text/ParserComb.hs:(66,1)-(68,12) 6.3 0.0
foldr.rec Data.List lib/Data/List.hs:(57,5)-(60,30) 4.2 2.8
...
Read this list straight: it's dominated by Text.ParserComb's >>=,
Data.List.(++), and Text.String.eqString β Haskell functions inside
the compiler's own parser and prelude, measured on bootuhs, a native
GHC binary running the "local lib" build. Not one combinator name β S,
K, B, O, the names 04a4bbe4's primops[]/primTable reorder β
appears anywhere in this report, because they belong to a different
program entirely: eval.c's interpreter and Translate.hs's translator,
neither of which bootuhs's GHC profiler ever runs. The "connect the
profiling flag to the frequency ordering" instinct doesn't survive this
run: they're two separate performance interventions, aimed at two
separate programs, that happen to land in the same 48-hour window. What
this report does support independently is 8f361a0a's "Fix stupid
eqString" from not quite two days earlier β eqString alone accounts for 8.7% of
this run's time and 12.9% of its allocation, exactly the kind of
hot-path cost a workload this size makes visible for the first time.
Clean up both worktrees:
git worktree remove --force /tmp/mhs-456ce17c
git worktree remove --force /tmp/mhs-04a4bbe4