21. Beating C honestly

Every claim in this chapter’s title has been on the book’s pages for eighteen chapters. Chapter 7 proved that mut is an exclusive claim and a bare parameter a shared read. Chapter 8 priced a region at a pointer bump per allocation and one wholesale free. Chapter 3 trapped an overflow and promised the trap survives release builds. None of those chapters put a stopwatch next to the proof, because a stopwatch is an instrument, and this book does not print a number its CI did not produce.

So this chapter has one rule, stated up front because it is also the subject: a performance number on these pages is a CI artifact with a date and a commit, quoted with its provenance, or it does not appear. The argument was Parts 2 and 3; what follows is the accounting.

21.1 Aliasing

Here is the oldest kernel in the comparison business, in C, written the way an expert writes it:

void saxpy(double a, const double *restrict xs, double *restrict ys, size_t n) {
    for (size_t i = 0; i < n; i++) ys[i] = a * xs[i] + ys[i];
}
$ ./saxpy_restrict
12 20

The interesting word is restrict, for what it does not do. Without it, the compiler must plan for xs and ys overlapping: a store through ys[i] could change a later xs[j], so vectorizing the loop means either a runtime overlap check or giving up the transform. restrict is the programmer promising disjointness. Nothing checks the promise. Call this function with overlapping pointers and the program is undefined: no diagnostic, no trap, whatever the optimizer’s assumptions happened to compile to.

The same kernel in wolf carries the same fact in its signature:

fn saxpy(a: f64, xs: List[f64], mut ys: List[f64]) {
    var i = 0
    while i < xs.len {
        ys[i] = a * xs[i] + ys[i]
        i += 1
    }
}
fn main() -> !int {
    var xs = List[f64]()
    (mut xs).push(1.0)
    (mut xs).push(2.0)
    (mut xs).push(3.0)
    (mut xs).push(4.0)
    (mut xs).push(5.0)
    var ys = List[f64]()
    var i = 0
    while i < 5 { (mut ys).push(10.0); i += 1 }
    saxpy(2.0, xs, mut ys)
    print("{ys[0]} {ys[4]}")
    0
}
$ wolf build --release saxpy.lu
$ ./saxpy
12 20
$ lupin saxpy.lu
12 20

Both machines, both tiers, one answer: 12 20 is 2·1+10 and 2·5+10, printed the way a whole-valued f64 prints. mut ys is chapter 7’s exclusive claim and xs its shared read, so the disjointness restrict promises is here a fact the caller demonstrated at the call site. Same information reaching the optimizer, different laborer: C trusts the programmer’s word, wolf makes the caller show its work. And because it is checked rather than promised, the lie is unwritable:

fn saxpy(a: f64, xs: List[f64], mut ys: List[f64]) {
    var i = 0
    while i < xs.len {
        ys[i] = a * xs[i] + ys[i]
        i += 1
    }
}
fn main() -> !int {
    var xs = List[f64]()
    (mut xs).push(1.0)
    saxpy(2.0, xs, mut xs)
    0
}
error[E1002]: `xs` is lent for this call while `xs` goes `mut` in it
  --> ./s2.lu:14:16
   |
14 |     saxpy(2.0, xs, mut xs)
   |                ^^ read of a place being mutated
   |                        -- `xs` is passed `mut` here
   |
   = note: the same place twice is never disjoint.

The C version of this call compiles without comment and runs into undefined behavior; the wolf version is refused in one line whose note is the whole aliasing story: the same place twice is never disjoint. The interpreter enforces the identical rule at its own moment —

$ lupin clash.lu
clash.lu: trap(exclusivity): `xs` is accessed as `mut` while `xs` is held as `read`; the paths conflict [mem.tier0.excl.1] at 11:20; `xs` held here at 11:16
$ echo $?
3

— which is the difference between “this could alias” and “this did.” Prose cannot settle whether the proved fact buys what the promised fact buys; the benchmark suite in §21.4 exists to keep answering it, and its aliasing family runs exactly this kernel shape.

Exercise 21-1 (comprehension · prose). Here is saxpy in C again, with the promise withheld:

void saxpy(double a, const double *xs, double *ys, size_t n) {
    for (size_t i = 0; i < n; i++) ys[i] = a * xs[i] + ys[i];
}

Without restrict, name the specific possibility the C compiler must plan for, and the optimization it therefore hesitates on. Then state what a wolf compiler knows about saxpy(a, xs, mut ys) from the signature alone, and who did the work of establishing it.

Exercise 21-2 (fingers · lupin). Type the wolf saxpy and run it: five elements, a = 2.0, ys all tens. Predict both printed values first.

21.2 Arenas

Chapter 8 taught the region as a safety story. This section reads the same brace as a price list, over one pass’s worth of work: build ten thousand nodes, read them, let them go:

struct Node { value: int, weight: int }
fn main() -> !int {
    var total = 0
    region pass {
        var nodes = List[Node]()
        var i = 0
        while i < 10000 {
            (mut nodes).push(Node { value: i, weight: i % 7 })
            i += 1
        }
        var j = 0
        while j < nodes.len {
            total += nodes[j].weight
            j += 1
        }
    }
    print("{total}")
    0
}
$ wolf build --release pass.lu
$ ./pass
29994
$ lupin pass.lu
29994

The C discipline for the same pass allocates retail and frees retail:

    for (int i = 0; i < 10000; i++) {
        nodes[i] = malloc(sizeof(struct node));
        if (nodes[i] == NULL)
            return 1;
        nodes[i]->value = i;
        nodes[i]->weight = i % 7;
    }
    for (int i = 0; i < 10000; i++)
        total += nodes[i]->weight;
    for (int i = 0; i < 10000; i++)
        free(nodes[i]);
$ ./arena_nodes
29994

Now count, because this arithmetic is the section and you can check it without a stopwatch. The C pass makes 10,000 calls into malloc and 10,000 into free: 20,000 allocator interactions, each one a real function with real bookkeeping, for data whose entire lifetime is one pass. The region pass makes one region, bumps a pointer ten thousand times (§8.3 named the strategy: bump a pointer, free the lot), grows the list’s buffer a handful of times, and frees everything in one motion at the brace. Every C programmer who has profiled a loop like this knows the next move: write an arena. The C arena gets the same allocator math, and it gets it by convention: the rule that no node pointer outlives the arena is enforced by code review. Chapter 8 spent a chapter on who enforces it in wolf: the escape is a compile error with a name, E1010, and the teardown you watched above is not a loop anyone had to remember to write.

So the honest comparison is “checked arena versus disciplined arena”: an arena wins “region versus malloc” in any language, which is why every allocator-heavy C codebase grows one. The allocator math is identical, and the difference priced in this section is who catches the escapee. The suite’s arena family in §21.4 runs this allocation pattern against malloc discipline; what it cannot measure is the code review.

Exercise 21-3 (comprehension · prose). A request handler builds a parse tree of 10,000 nodes, reads it, and discards it. Count the allocator interactions (calls into allocate and free machinery) for (a) malloc discipline with individual free, (b) malloc discipline with one arena library, (c) a wolf region. Then name the cost in (c) that did not disappear and where it went.

21.4 Checked arithmetic’s bill

Chapter 3 made a promise this book has repeated in every part: checked arithmetic in every profile. X3, a decision, not a mood. Here is the promise audited on the tier where it costs the most. sum_to(n) adds a million to an i32 accumulator n times:

fn sum_to(n: int) -> i32 {
    var acc: i32 = 0
    var i = 0
    while i < n {
        acc = acc + 1000000
        i += 1
    }
    acc
}
fn main() -> !int {
    print("{sum_to(2000)}")
    print("{sum_to(3000)}")
    0
}

sum_to(2000) reaches 2.0 × 10⁹, under i32’s 2,147,483,647 ceiling. sum_to(3000) crosses it at iteration 2148, and the release binary, the one a benchmark would time, refuses to look away:

$ wolf build --release bill.lu
$ ./bill
2000000000
wolf-trap: overflow
  at ./bill.lu:12:5

The compiled report names the kind and the site; the interpreter’s names everything:

$ lupin bill.lu
2000000000
bill.lu: trap(overflow): `+` produced 2148000000, outside `i32` — checked arithmetic traps in every profile (X3); spell intended overflow `wrapping[i32]` [arith.checked] at 5:15
$ echo $?
3

Four thousand and change additions ran before that trap, and every one of them carried the check that made the last one honest. That is the bill. What the bill comes to after an optimizer has hoisted, folded, and branch-predicted its way through it is a measured number, and this is where the chapter’s opening rule pays out. The number exists, it is CI’s, and it has a date.

The compiler’s repository runs a benchmark suite of thirteen kernels in five families (aliasing, arenas, layout, strings, checked arithmetic), each one idiomatic wolf against the naive C a working programmer actually writes, compiled clang -O3. No hand-tuning on either side; serial kernels only, no concurrent family. The gate is public: suite geometric-mean speedup at or above 1.00, no single kernel losing by more than ten percent unless a written exception documents exactly why, exceptions capped at three. The suite runs nightly, and each night’s verdict is appended to a ledger in the repository, bench/ritual-ledger.jsonl. The line this edition cites, recorded the night this page was written:

{"ts":"2026-08-24T03:39Z","commit":"2ff4dd1","geomean":1.080,"verdict":"HOLDS","consecutive":3}

Read it the way you would read any measurement: on 2026-08-24, at commit 2ff4dd1, idiomatic checked wolf was eight percent faster than naive clang -O3 across the suite’s geometric mean, the gate held, and it was the third consecutive nightly hold: the streak that turns one good night into a declaration. The declaration and its full paper trail are wolf-lang#89. When the suite regresses, the ledger will say so with the same precision, which is the entire reason to cite a ledger instead of a launch post.

Two kernels lose, and the exceptions file is the best reading in the suite. b3_churn loses because its request loop does arithmetic on values that arrive from the request itself, and no sound analysis can prove those additions in-range, so they stay checked, at a measured 83 instructions per request, 19.7 percent of the kernel’s hot function. That is this section’s bill, priced by the suite’s own A/B against a wrapping[int] twin of the same kernel, and wolf pays it on purpose: the alternative is a language that turns off its arithmetic checks the moment anyone starts timing it, which is the benchmark rigging this chapter’s title forswears. word_count loses for a different and humbler reason: its string iterator locates every word boundary while naive C counts state transitions without ever finding one, a data-dependent branch per byte against a branchless loop. Neither exception is hidden in an average: the gate reads the exceptions file, counts it against the cap, and fails the night the file stops telling the truth.

Exercise 21-5 (comprehension · lupin). The bill and the payout in one program: sum_to(n) adds 1,000,000 to an i32 accumulator n times. Predict both calls’ fates (sum_to(2000), then sum_to(3000)) with the arithmetic that decides them.

Exercise 21-6 (spelunking · lupin). From exercise 21-5’s trap line alone: name the decision id it cites, the clause tag it enforces, and the documented spelling for the program that wanted wraparound. Then state, in one sentence, why this trap firing “in every profile” is the chapter’s honesty rather than the chapter’s embarrassment.

Exercise 21-9 (comprehension · prose). “Beats naive C, and the claim is a falsifiable CI gate” is a sentence with a specific engineering content. Name the three artifacts that must exist for the claim to be falsifiable, not promotional, and for each say whether this edition already prints it.

Two of this chapter’s five sections are reserved, and the colophon’s count includes them. §21.3’s benchmark is a traversal over Soa[T], a struct-of-arrays type built by compile-time reflection; the type-building half of that machinery has no spelling in the language this edition names, so the section’s kernel cannot be written by a reader, and a benchmark the reader cannot write is not one this book prints. §21.5 is the loss table: every kernel currently behind C, each with its tracking issue, regenerated from the bench instrument on every release. The toolchain this edition names does not ship that instrument, and a loss table that cannot be regenerated is exactly the kind of number this chapter refuses to print once, by hand, hopefully. The suite’s ledger and exceptions file carry the losses with full honesty in the meantime, one repository over from this book.