Spilling
When there is no register left, a value goes to a stack slot and every use becomes a memory access. The interesting question is never whether to spill but which value — and our engine reports the reason rather than just the outcome.
What happens when the allocator runs out of registers, and how does it choose the loser?
The allocation with holes in it: a partial map from virtual registers to physical ones, plus a set of values assigned to stack slots instead. Spilling changes the program, not just the mapping — a spilled value acquires a load before every use and a store after every definition, and those new instructions have live ranges of their own, which is why the representation is unstable and the allocator usually has to start again.
Replacing a register with a stack slot is always legal for an ordinary SSA value: the value is the same, only its storage differs. The obligations are that every use is preceded by a load from the slot and every definition followed by a store to it, that the slot is not reused by another value with an overlapping range, and that the frame is large enough and correctly aligned. It stops being purely a storage decision when the value is a live reference in a garbage-collected runtime, since the slot must then appear in the stack map, or when the address of the slot escapes, which reintroduces every aliasing question the value did not previously have.
Key points
- A spill turns a free register operand into a load before every use and a store after every definition.
- Above the peak-liveness bound, spilling is not a failure. Which value is chosen is the decision that matters.
- The classic heuristic is uses per unit of live range, penalised for crossing a call — a value crossing a call costs memory traffic either way.
- Weighting uses by estimated execution frequency is essential, and an unweighted heuristic will happily spill the inner-loop value.
- Spilling changes the program: reload temporaries create new pressure, so real allocators rebuild and retry until they converge.
- Live-range splitting — register where the value is used, memory where it is not — is strictly better than an all-or-nothing spill.
- The pressure was created upstream by inlining, unrolling and scheduling. That is usually where the fix is.
What a spill actually costs
[rbp-8] addressing and the mov mnemonics are x86-64. On AArch64 the same reload is ldr x0, [x29, #-8] and stores are str, and the offset must satisfy the addressing mode's range and alignment rules — which occasionally forces an extra instruction to materialise the offset, a cost x86-64's wider displacement field does not have.A register operand is part of the instruction and costs nothing extra. A spilled value costs a mov from memory before each use and a mov to memory after each definition, and those instructions have consequences beyond their count: they lengthen the dependence chain through the value, they consume load and store issue ports, and they occupy stack space that may or may not be in cache.
The good case is not terrible. A spill slot in the current stack frame is almost certainly in L1, and store-to-load forwarding on a modern core can satisfy a reload from a very recent store without going to cache at all. The bad case is genuinely bad: a spill inside a loop, reloaded every iteration, on a working set that has evicted the frame from L1. That is the difference between a few percent and several times slower, and it is why "the loop spills" is a real diagnosis rather than a stylistic complaint.
The transformation below is what a spill looks like in the instruction stream. Note the shape: two extra instructions for one use, and the arithmetic that used to be one instruction is now three with a memory dependence in the middle.
%6 = %5 + %2 when %2 is spilledadd rcx, rdx ; %6 = %5 + %2, both in registers
mov rax, [rbp-8] ; reload %2 from its stack slot add rcx, rax ; %6 = %5 + %2
Always legal for an ordinary value: the stack slot holds exactly what the register held, so the computed result is identical. The obligations are mechanical — a store after the definition, a load before each use, a slot that no overlapping value also uses, and a frame large enough and aligned as the ABI requires.
It is not merely a storage change when the value is a live reference in a garbage-collected runtime: the slot must be recorded in the stack map for that safepoint, or the collector will not see the reference and will free or fail to relocate a live object. It is also wrong to spill into a slot whose address has escaped, and wrong to spill across a setjmp boundary or an asynchronous signal without accounting for what may observe the frame.
Choosing the loser
Since spilling is unavoidable above the peak-pressure bound, the decision that matters is which value loses. The heuristic in our engine is the classic one: cost is uses divided by the length of the live range, with a penalty for crossing a call, and the value with the lowest cost is spilled. Read it as "how much does this value get out of the register it is occupying" — a value read twenty times in a five-instruction range is earning its place, and one read twice across fifty instructions is not.
The call penalty deserves a sentence of its own. A value live across a call must either sit in a callee-saved register — which obliges the function to save and restore it — or be spilled around the call anyway. Since it is going to cost memory traffic in either case, it is a cheaper spill candidate than an equivalent value that does not cross a call. Our engine encodes that by reducing its cost, making it more likely to be chosen.
What our heuristic is missing is loop depth, and that omission is the most important thing to know about spill heuristics generally. A use inside a loop executed a thousand times is worth a thousand uses outside it, and every production allocator weights uses by estimated execution frequency — from static heuristics (loops are hot, error paths are cold) or from a real profile. An allocator that counts uses without weighting them will happily spill the inner-loop value and keep the one used once at function entry.
Read it asThe engine records: "%2 is live across 4 instructions with only 1 use, so it costs the least to keep in memory." That sentence is the point. An allocator that reports only *that* a value spilled leaves the reader with nothing to act on; one that reports *why* tells them the fix is to shorten that range or to reduce the pressure that made three registers insufficient.
Spilling changes the problem
The awkward property of spilling is that it is not the end of the allocation — it is a modification of the program that the allocator must then re-analyse. Each inserted reload defines a new short-lived value that needs a register of its own, right at the point where pressure was already at its peak. Insert enough of them and the new temporaries create pressure that requires further spilling.
This is why a production Chaitin-Briggs allocator is a loop: build the graph, try to colour, insert spill code for whatever failed, rebuild, try again. It normally converges in two or three rounds because the reload temporaries are extremely short-lived and easy to colour, but the loop is genuinely necessary and an allocator that spills once and stops — as ours does — is producing an allocation that has not been checked against its own output.
The better answer, where it can be afforded, is not to make the decision all-or-nothing. *Live-range splitting* keeps a value in a register through the region where it is used and in memory through the region where it is not, inserting copies at the boundaries. That is strictly more expressive than spilling the whole range, and it is the single largest quality difference between a textbook allocator and a production one.
- Every reload is a new definition with a new, tiny live range at the point of highest pressure.
- Every store is an instruction the scheduler must now order against surrounding memory operations.
- The stack frame grows, which changes offsets and can change the prologue — see
[[stack-frame-layout]]. - A spilled reference in a garbage-collected runtime must be recorded in the stack map for every safepoint it is live across.
- The allocator must rebuild and retry, because the code it is allocating for is no longer the code it analysed.
Where the pressure came from
The most useful thing to know about spilling is that the allocator is rarely the right place to fix it. Register pressure is manufactured upstream, and by the time the allocator sees it the decision has been made by someone else.
Inlining is the largest source: bringing a callee's body into a loop brings its live values with it, and an inliner working to a size budget has no model of register pressure at all. Unrolling multiplies the working set by the unroll factor. Vectorization consumes vector registers and often forces integer pressure up alongside. And the scheduler, hoisting loads to hide latency, deliberately extends live ranges — which is the clearest example of two phases with opposite objectives running one after the other.
So when a hot loop spills, the productive question is which upstream decision put too many values in flight, not which allocator flag to turn. Reducing the inline budget for that call site, capping the unroll factor, or restructuring so the inner loop touches fewer values are all interventions that work; asking the allocator to be cleverer usually is not.
How it works
The steps, in the order the compiler takes them.
- When no register is available for a value, assign it a stack slot in the current frame instead.
- Insert a store to the slot after every definition of the value, and a load from the slot before every use.
- Enlarge the frame accordingly and adjust the prologue, keeping the ABI's alignment requirement at every call.
- Record the slot in any metadata that must know where values live — debug location lists, and garbage-collector stack maps if the value is a reference.
- Recompute live ranges, since the reloads are new definitions with their own ranges, and re-run allocation until no new spills are produced.
- Where the allocator supports it, split the range instead: keep a register through the high-use region and use memory elsewhere, with copies at the boundaries.
How it breaks
What the engineer observes when it goes wrong — not what goes wrong internally.
- A frequently-used value in an inner loop is spilled because the heuristic counted uses without weighting by loop depth, and the loop is several times slower than it needs to be.
- Two values with overlapping ranges are assigned the same stack slot, and one silently overwrites the other. No crash, wrong answers.
- The frame is enlarged without maintaining the ABI's stack alignment at call sites, and a callee using aligned vector loads faults — a crash whose cause is many frames away from its symptom.
- A spilled reference is not recorded in the garbage collector's stack map, and the collector frees or fails to relocate a live object. The resulting corruption appears at an unrelated allocation much later.
- Spill code is inserted but the allocator does not re-run, the reload temporaries have no registers of their own, and the compiler either crashes or emits code that clobbers something.
- Debug information is not updated for the spilled value, and the debugger reports a stale register location — showing a confidently wrong value rather than admitting it does not know.
When it helps
- It is not an optimization to be enabled — it is what makes allocation total. Without it, a function whose pressure exceeds the register count could not be compiled at all.
- Values that are genuinely cold: something computed at function entry and used once at the end is better in memory than occupying a register throughout.
- Values live across many calls, which would otherwise force the function to save and restore callee-saved registers it does not otherwise need.
When it hurts
- Inner loops, where a reload every iteration turns a register access into memory traffic on the hottest path in the program.
- Long dependence chains, where the extra load latency is added directly to the critical path rather than absorbed.
- Deeply recursive code, where every frame carries its spill slots and the aggregate stack footprint grows accordingly.
What it costs
Every one of these is paid by something.
- Spilling a value buys a register for something else and costs two instructions per use, a longer dependence chain and stack space — a good trade for a cold value and a bad one for a hot one, with nothing in the mechanism to tell them apart.
- A more accurate spill heuristic (frequency weighting, real profile data) buys much better choices and costs either a static estimator that is sometimes wrong or a profiling build step that is operationally expensive.
- Live-range splitting buys most of the difference between a textbook and a production allocator, and costs considerable implementation complexity plus copies at every split point that must then be coalesced or removed.
What else you could do
What a different compiler or language does instead, and when that is better.
- Rematerialization: recompute a cheap value at each use instead of storing and reloading it. A constant, an address computation or a load from a known-immutable location is often cheaper to redo than to spill — see
[[coalescing-and-rematerialization]]. - Live-range splitting, which spills only the part of a range that is not paying for itself.
- Use a callee-saved register: pay one push and one pop for the whole function instead of a load and a store per use. Better whenever the value is used more than a couple of times.
- Reduce the pressure upstream — less inlining into this loop, a smaller unroll factor, restructuring the inner loop to touch fewer values. Usually the intervention with the largest effect.
See it for yourself
The flag, dump or tool that shows you this directly.
- Count them:
llc -stats file.ll 2>&1 | grep -iE "spill|reload"reports the numbers LLVM inserted. - Find them in the output: look for
movto and from[rbp-N]or[rsp+N]inside loop bodies inclang -O2 -Soutput. Spill slots cluster at negative frame-pointer offsets. - Confirm the cause: rebuild with
-fno-inlineor a reduced unroll pragma and see whether the stack traffic disappears — that identifies the upstream source rather than the allocator. - Measure the effect rather than inferring it:
perf stat -e mem_inst_retired.all_loadsbefore and after, on the hot function alone. - Ours:
allocateByColoringandallocateByLinearScanboth return aspillReasonsmap explaining each choice in words;/compilers/registersdisplays it beside the chart.
Plausible wrong readings
Stated the way a confident engineer states them.
- "Spilling means the compiler ran out of registers." It means this value lost the competition for them. Above peak pressure, some value had to.
- "A spill is a memory access, so it is hundreds of cycles." A spill slot in the current frame is nearly always in L1, and a reload immediately after a store is usually satisfied by store-to-load forwarding. The bad case is real; it is not the typical case.
- "The allocator should just be smarter." Often it should, but the pressure it is resolving was created by inlining and unrolling decisions made much earlier, and no allocator can un-inline.
- "Spilled means the value lives in memory for the whole function." Only without live-range splitting. With splitting, a value can be in a register exactly where it is used and in memory elsewhere.
Misconceptions
The claim, and what is actually true.
Go deeper
The same idea at increasing depth. Stop wherever it stops being useful.
overview
When more values are alive at once than there are registers, someone has to live in memory instead. That value gets a slot in the function's stack frame, and from then on every read of it is a load and every write is a store. The compiler picks the value that gets the least out of its register: long-lived, rarely used, and preferably one that was going to cost memory traffic across a call anyway.
practical
Loads and stores of stack offsets inside a loop body are spill code, and finding them is a two-minute diagnosis in clang -O2 -S output. The fix is almost always upstream: reduce what is live at once. Try the loop with less inlining or a smaller unroll factor and see whether the traffic disappears. If it does, you have identified the real decision; if it does not, the working set of the loop itself is too large and needs restructuring.
advanced
The framing worth carrying is that spilling is where three phases send each other the bill. The inliner spends register pressure to remove call overhead and has no pressure model. The scheduler spends register pressure to hide latency and has, at best, a partial one. The allocator receives the total and can only decide who pays. Every serious attempt to improve generated code at this level has been an attempt to move information backwards across those boundaries — pressure-aware scheduling, inlining heuristics that account for callee liveness, integrated approaches that decide selection and allocation together. The persistent difficulty is that the information each phase needs is produced by a phase that runs after it, and no amount of cleverness in the allocator closes that gap.
How much this depends on
Nothing in this domain is true of every compiler. These say how much.
[rbp-8] form is x86-64; AArch64 uses [x29, #-8] with a narrower immediate range that occasionally costs an extra instruction.If you were asked this in an interview
- What does a spill cost, precisely, and when is that cost small?
- How would you decide which value to spill, and what does the naive heuristic get wrong?
- Why does inserting spill code usually mean re-running the allocator?
Connections
- Programming Languages & Runtime Internals — Stack maps: which frame slots hold live references at a safepointSpilling a reference in a managed runtime is not merely a storage decision — the collector must be told the slot holds a root, or it will free or fail to relocate a live object. The collector's side of that contract is owned there; the obligation to emit the map is the compiler's.