A CPU has a fixed, tiny set of registers — the fastest memory it owns. When a computation needs more live values than there are registers, the compiler spills the overflow to the stack. So: what if we take registers away on purpose, one at a time, and watch the spills — and the clock — climb?
Stock gcc accepts a flag, -ffixed-<reg>, that removes a register from the allocator's pool for good.
Reserve r15 and the compiler pretends it does not exist; reserve r15 r14 r13 r12 and it must make do with four fewer.
We compile each kernel again and again, handing back one more register each pass, and for every version we read the spill count straight out of the disassembly and time the workload on this machine.
Two register files, two colours throughout: the general-purpose integer registers that hashes and ciphers live in, and the XMM vector registers that floating-point math uses. Nine kernels, chosen to span the spectrum from "desperate for registers" to "barely cares." Every version is checked against a known answer first — starving the allocator never changes the result, only the cost.
Drag to reserve registers from the top of each file. Watch which the compiler is forced to give up — and everything below reacts.
Slowdown at the current setting, versus each kernel run with its full register file. Sorted worst-first — it re-sorts as you drag.
Each card traces spills (area) and time (line) across the whole range, full at left, starved at right. The dot marks the current setting. Click one to feature it below.
One gcc -O2 compile per budget, adding -ffixed-<reg> for each reserved register. GP kernels give up r8–r15, rbx, rbp; XMM kernels give up xmm4–xmm15. The ABI registers (rax/rcx/rdx/rsi/rdi, xmm0–3) are never touched.
Read from the hot function's disassembly: every instruction that touches a fixed stack slot — …(%rsp) — counted, excluding indexed array access. A proxy for spill/reload traffic, deterministic and compiler-honest.
A Rust harness dlopens each build and times the whole workload with a monotonic clock, taking the minimum over repetitions on one pinned core — the run least disturbed by the scheduler. Hardware perf counters are sandboxed off here, so: wall-clock.
Before timing, every build runs a known-answer test (RFC vectors, reference matmuls, compress/decompress round-trips). All nine kernels return bit-identical results at every budget — verified.