Register deprivation: spills and runtime under forced register scarcity
Register deprivation: spills and runtime under forced register scarcity
Intel Xeon E-2236 @ 3.40GHz · gcc 15.2.0 · 2026-07-13
We compiled nine small kernels while progressively reserving registers with<br>gcc -ffixed-, then measured spills and runtime on one machine. Reserving<br>registers reliably increases spills, but the static spill count is a weak<br>predictor of the runtime cost, and one kernel gains 23 spills with no slowdown<br>at all.
Code is at https://github.com/rjpower/spillbench; the numbers behind every<br>figure below are in results.json.
TL;DR
Removing registers slows 8 of 9 kernels by 14–76% at the tightest budget; the<br>ninth (SipHash) does not move. Results are bit-identical at every budget, so<br>the reservation only changes generated code, not the computation.
Added spill instructions correlate weakly with slowdown (Pearson r = 0.55 over<br>105 points). The cost of one added spill ranges from about 0%/spill (SipHash)<br>to 2.2%/spill (FIR filter), a ~30× spread.
The effect is specific to the register file the kernel uses. Reserving XMM<br>registers on SHA-256 costs +5.6%; reserving GP registers costs +33%. Reserving<br>GP registers on a double-precision matmul is flat; reserving XMM costs +34%.
GCC auto-vectorizes part of SHA-256 with SSE at -O2, so reserving XMM is not<br>inert for that integer kernel: its stack-spill count rises 8→65 while runtime<br>moves +5.6%.
Measured with gcc 15.2.0 -O2 on an Intel Xeon E-2236, wall-clock, minimum of<br>15 repetitions on one pinned core.
Setup
We test three hypotheses:
H1: fewer registers produce more spills and slower code, monotonically in the<br>number of registers removed.
H2: the slowdown is specific to the register file the kernel actually uses<br>(integer kernels to GP registers, floating-point kernels to XMM).
H3: the static spill count predicts the runtime cost.
We reserve registers with gcc -ffixed-, which removes a register from the<br>allocator's pool for the whole compilation. We recompile each kernel once per<br>budget, adding one reservation each pass. GP kernels give up<br>r15 r14 r13 r12 rbp rbx r11 r10 r9 r8 in that order (15 allocatable registers<br>down to 5; rsp is never allocatable). XMM kernels give up xmm15 … xmm4 (16<br>down to 4). We never reserve the ABI registers (rax rcx rdx rsi rdi, xmm0–3),<br>which argument passing and instructions like mul and variable shifts require.
Six integer kernels exercise the GP file, three floating-point kernels exercise<br>the XMM file. Each is a single C function with a known-answer test.
KernelFileWhat it isCorrectness check<br>ChaCha20GP20-round stream cipher block, 16×32-bit stateRFC 8439 §2.3.2 keystream<br>SHA-256GPcompression function, 8 vars + 64-word scheduleSHA-256("abc")<br>SipHash-2-4GPkeyed hash, 4×64-bit ARXreference vector (15-byte msg)<br>Integer matmulGP4×4 register-blocked int64 micro-kernelvs. naive triple loop<br>LZ77 compressGPhash-chain match findercompress→decompress round-trip<br>QuicksortGPin-place recursive sort of int32output is sorted<br>Double matmulXMM4×4 register-blocked f64 micro-kernelvs. naive triple loop<br>FIR filterXMM16-tap filter, 8 output lanesvs. naive tap sum<br>MandelbrotXMMescape-time, few live doubles per pixelescape counts at known points
For the spill metric, we disassemble the hot function (objdump) and count<br>instructions that reference a fixed stack slot (…(%rsp)), excluding indexed<br>array access. This is a static proxy for spill/reload traffic. We do not have<br>dynamic spill counts: hardware performance counters are disabled in this<br>environment (perf_event_paranoid=4).
For timing, a Rust harness dlopens each build, runs its known-answer test, and<br>times the whole workload with a monotonic clock, keeping the minimum over 15<br>repetitions on one pinned core. Each kernel's workload is sized to ~100 ms.<br>Machine: Intel Xeon E-2236 @ 3.40 GHz, gcc 15.2.0, -O2 -fPIC -shared.
Every build passes its known-answer test, and every kernel returns the same<br>checksum at all budgets. Reservation is a pure allocation constraint here; it<br>never changed a result.
Result 1: removing registers slows most kernels, but not all
Eight of nine kernels slow down as registers are removed; SipHash-2-4 does not.
Figure 1. Runtime vs registers reserved, integer and float kernels<br>Figure 2. Spill count vs registers reserved<br>Figure 3. Slowdown at the tightest register budget<br>KernelFileRuntime full→min (ms)SlowdownSpills full→min<br>FIR filterXMM135.5 → 238.9+76%8 → 43<br>Integer matmulGP140.5 → 199.7+42%60 → 82<br>Double matmulXMM131.5 → 176.4+34%66 → 100<br>SHA-256GP89.4 → 119.0+33%8 → 80<br>QuicksortGP134.2 → 171.7+28%4 → 49<br>ChaCha20GP88.5 → 110.0+24%55 → 121<br>LZ77 compressGP123.2 → 146.8+19%39 → 115<br>MandelbrotXMM101.6 → 116.2+14%0 → 7<br>SipHash-2-4GP79.4 → 78.1−2%0 → 23
Spill counts rise for all nine kernels. Runtime rises for eight. H1 holds in<br>direction but not in shape: neither curve is cleanly monotonic in the budget.<br>Static spills drop when a register is removed in a few cases...