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.