19. Reading the release tier

Chapter 1 put two machines on your desk and one rule between them: same program, same meaning. Every binary since has come from wolf build, whose job is to be honest quickly. This chapter is about the tier whose job is to be honest fast, and about how to read what it did, because a compiler that makes your program faster is making claims, and claims are for checking.

19.1 One program, two binaries

Here is a program with a real loop in it, a two-state scanner that counts words the way a C programmer counts them, one byte at a time:

fn count_words(text: str) -> int {
    var n = 0
    var in_word = false
    for b in text.bytes() {
        let inside = b as int != 32 && b as int != 10
        if inside && !in_word { n += 1 }
        in_word = inside
    }
    n
}
fn main() -> !int {
    let line = "the pack hunts at dusk and sleeps at noon"
    print("{count_words(line)} words")
    0
}

One flag asks for the optimizing tier:

$ wolf build --release scan.lu
$ ./scan
9 words
$ lupin scan.lu
9 words

The flag changes how the binary is made, and it is not allowed to change what the binary means. The answer is the same, and so is every rule this book has taught. The checks come along too:

fn main() -> !int {
    var xs = List[int]()
    (mut xs).push(7)
    print("{xs[3]}")
    0
}
$ wolf build --release oops.lu
$ ./oops
wolf-trap: bounds
  at ./oops.lu:4:5
$ lupin oops.lu
oops.lu: trap(bounds): index 3 is outside a collection of 1 element(s) [mem.ub.defined] at 4:13
$ echo $?
3

An optimized binary that indexed past the end and kept going would be a faster way to be wrong. This one stops, names the kind, and names the site. What --release changes is the work between your source and the instruction stream, and the interesting part is where the speed comes from. It comes from facts you already wrote. A bare parameter is a shared read and mut is an exclusive claim, so the compiler knows two buffers are disjoint without an alias analysis heroic enough to guess it. A bounds check that guards a loop is a range every later check in the loop can be measured against, and a check the compiler can prove redundant folds away: the same analysis that keeps xs[3] honest deletes the checks that were never going to fire. A container’s header and its elements are separate allocations, so a store through one cannot alias the other. Chapter 8 called this shape a fact the optimizer can spend, and the release tier is where the spending happens: the compiler’s own middle end folds, hoists, and versions on the facts the checks established, then hands what remains to LLVM, the same backend that compiles the C this language is measured against.

Exercise 19-1 (fingers · wolf + lupin). Type the scanner and change the line to "the pack hunts at dusk", with the doubled and tripled spaces. Predict the count on paper from the two-state rule, then build it with --release, run it, and run it under lupin.

19.2 The compiler hands LLVM less

Two compilers work in sequence on a release build, so there is an honest question about who does what. The project’s answer is measurable: the middle end’s job is to hand LLVM less. Every optimization it performs on its own IR (a folded bounds check, a deleted dead store, a call that became its callee) is instructions that never reach the backend at all.

CI tracks that as a ratio: the instructions handed to LLVM, divided by the instructions a naive translation of the same program would hand it. Over the compiler repository’s witness corpus, the non-concurrent programs measure 0.582, recorded in bench/gates.json: the middle end delivers a little more than half of what a direct lowering would. Over the thirteen benchmark kernels the same ratio is around 0.92, and the gap between those two numbers is itself informative. The corpus is ordinary programs, full of checks and structure the middle end can prove things about; a benchmark kernel is already a lean loop, and a lean loop has little to remove. The gate file also records the original design target, half, and records that it has not been met, which is the project keeping its own score honestly rather than moving the goalposts to wherever the ball landed.

One caution before you read that ratio as a speedometer: it is a measure of work delivered, not of speed. The number can move up while programs get faster, because some optimizations spend instructions to buy time. When the middle end versions a loop (a guarded fast copy whose checks fold, beside a slow copy that keeps them all), the program grows and the hot path shrinks. The ratio is one instrument among several, and the next section is about what the others say when a program is slow.

Exercise 19-2 (comprehension · prose). A release of the compiler makes one benchmark kernel 11 percent faster, and the same release moves that kernel’s handed-to-LLVM instruction count from 266 to 309. Explain how both numbers can be progress, and name the transformation shape from this section that produces exactly this signature.

19.3 Reading a loss

The compiler’s repository keeps a suite of thirteen small kernels, each one idiomatic wolf beside the naive C a working programmer would write, compiled clang -O3; chapter 21 reads its verdict in full. This section is about the discipline behind that verdict: what the project does when a kernel loses, because losing is where the instrument either teaches or lies.

The first rule is that a loss is diagnosed in the disassembly, not in the story you were hoping to tell. A wall-clock number says that a kernel is slow; only the instruction stream says what the processor was actually asked to do, and the diagnosis is not finished until a named mechanism accounts for the gap. Four kinds of slow cover every loss the suite has produced.

The fact went unspent. The compiler proved something (two buffers disjoint, a value in range) and the proof never reached the instruction that needed it. This is a pipeline bug: fix it, and keep the kernel as the regression test.

The fact does not exist. Nothing proved the needed claim, and nothing could: values that arrive from input have no range any sound analysis can assign. The check stays because it must.

The message was dropped. The fact was emitted and the backend ignored it. The metadata channel gets a test that fails when the message goes missing again.

The win was renounced. The missing speed requires undefined behavior this language refuses, or contradicts a semantic it chose on purpose. That is not a bug; it is a price, and it goes in a written exceptions file with a measured cost.

The second rule is that the fix is never to special-case the benchmark. A compiler that recognizes its own test suite is a student who memorized the answer key. The fix goes into the general pipeline with the kernel as its witness, or the loss stands and is written down.

Two worked losses show the rules paying. A three-point stencil sat at roughly a third of naive C’s speed while four diagnoses in a row each retired part of the gap: the surviving bounds guards, the overflow checks on its element sums, an alignment claim, and finally the loads themselves. The disassembly showed the fast loop issuing five loads and two stores for every four results, reloading across the loop’s back edge values it had read the iteration before; the C compiler was carrying that window in registers. Teaching the middle end to prove the two buffers disjoint (a runtime guard on the versioned loop’s door, so the proof is checked, not assumed) let the same window carry, and the loop now issues three loads and two stores per four results. CI pins the count as a floor, so the carry cannot silently rot. The kernel left the loss set. The second loss was cheaper to read: a string-search kernel ran at a ninth of naive C’s speed, and the disassembly showed one call surviving in the loop body, an out-of-line equality comparing five bytes. Inlining the compare moved the kernel to within 13 percent of C, a factor of eight from one measured mechanism. Both histories, with their dates, hosts, and per-run tables, are rows in the compiler repository’s loss ledger (bench/loss-ledger.md); nothing in them is folklore.

Read this way, the suite’s history is legible: on the night of 2026-08-21 the geometric mean crossed 1.000 for the first time, 1.072 against naive C at commit e75ee1e, and the ledger line for that run still says DOES-NOT-HOLD, because the gate reads more than the mean; the losses had not yet been ruled on. The mean is not the gate. Two kernels still lose today, on purpose and at a written price, and chapter 21 prints both price tags.

A last discipline, for the day you measure your own programs: a delta means nothing except against its own spread, a median resists the machine’s weather better than a mean, and a benchmark without a date and a commit is folklore. The suite’s every number carries all three, which is what makes its history readable at all.

Exercise 19-3 (comprehension · prose). Five timings of one benchmark, in nanoseconds: 1000, 1010, 990, 1005, 2400. Compute the mean and the median; state which one a careful report trusts and what the outlier most plausibly was. Then compute the median absolute deviation and say what it does with these five numbers that a standard deviation would not.

Exercise 19-4 (comprehension · prose). Classify each loss into one of this section’s four kinds, and name the fix path: (a) a kernel adds numbers that arrive over a socket, and the additions stay checked; (b) the compiler proves a parameter’s buffer disjoint from every other, and the loop still reloads it after each store; (c) a kernel would tie C if the language allowed reading one byte past the end of a buffer when the read provably lands in the same page.

Exercise 19-5 (design). A colleague’s slide says “our runtime is 8 percent faster than C.” Using this chapter’s last paragraph, list the three pieces of provenance the claim needs before it is a measurement, and state what the sentence quietly becomes as each one is removed.