Linear Scan Allocation
Sort the intervals by start point, sweep once, hand a register back whenever an interval ends, and when nothing is free spill whichever active interval ends last. Much faster than colouring, worse code — which is exactly the trade a JIT wants.
What does an allocator look like when compile time is the thing the user is waiting for?
Live intervals sorted by start point, plus two small mutable sets: the free registers and the currently active intervals ordered by end point. There is no graph. The representation deliberately forgets the pairwise conflict structure and keeps only "what is live right now", which is the sole reason the algorithm is linear.
Identical to colouring — no two simultaneously live values may share a register — but established differently. The sweep guarantees it structurally: a register is only returned to the free set when the interval holding it has ended before the current position, so anything in the free set is provably dead. The correctness argument is an invariant maintained by the sweep rather than a property checked against a graph, which is why the algorithm needs no verification step and why an off-by-one in the expiry test is a silent miscompilation.
Key points
- Sort intervals by start, sweep once, expire finished intervals back into the free set, allocate from it.
- When nothing is free, spill whichever of the newcomer and the latest-ending active interval ends last.
- No graph is built, so the cost is the sort plus a linear pass — an order of magnitude faster than colouring in the original measurements.
- The spill decision is positional rather than value-based, which is precisely the information the algorithm gave up by not building a graph.
- JITs use it because allocation time is paid on every compilation while a user waits, and ten percent worse code beats ten times slower compilation at the lower tiers.
- Modern versions add interval splitting and lifetime holes, which recovers most of the quality gap and costs the strict linearity.
One sweep, two sets
Sort intervals by start point. Walk them in order. At each interval, first expire: any active interval that ended before this one starts is finished, so return its register to the free set. Then allocate: if a register is free, take it and add this interval to the active set. If none is free, something must spill.
The spill rule is the interesting part and the one that gives the algorithm its character. Compare this interval with the active interval that ends *last*. Whichever of the two ends later gets spilled, because it is the one that would block a register for longer. If the incumbent loses, it hands its register to the newcomer and goes to memory; if the newcomer loses, it goes to memory immediately.
That is the entire algorithm. No graph, no simplification stack, no iteration. The cost is dominated by the sort — O(n log n) — and the sweep is linear in the number of intervals with a small constant. Poletto and Sarkar's 1999 paper reported it running an order of magnitude faster than colouring while producing code within about ten percent, and that ratio is why every JIT baseline and many optimizing tiers use it.
1for interval in sort_by_start(intervals):2 # 1. expire: anything that ended before we start is done3 for a in active:4 if a.to < interval.from:5 free.push(register_of(a))6 active.remove(a)7 8 # 2. allocate9 if free is not empty:10 assign(interval, free.pop())11 active.add(interval) # kept sorted by end point12 else:13 # 3. spill whichever ends last14 longest = active.last() # the latest-ending active interval15 if longest.to > interval.to:16 assign(interval, register_of(longest))17 spill(longest)18 active.replace(longest, interval)19 else:20 spill(interval)The active list is kept sorted by end point so that active.last() is the latest-ending interval in constant time. That ordering is the only data structure in the algorithm, and it is why the whole thing fits on a slide.
The same function, two algorithms
Here is the comparison the module exists to make. The function below has a call in it, six values, and only two registers available. Both algorithms spill exactly two values — but not the same two, and for entirely different stated reasons.
Graph colouring spills %0 and %2: both are long-lived with few uses, which is what its cost heuristic — uses per unit of live range — selects for. It spilled the values that were cheapest to keep in memory. Linear scan spills %2 and %3: at the moment each of them arrives, nothing is free and every active interval ends later, so they lose by position rather than by value. It spilled whatever happened to arrive when the registers were full.
That difference is the whole character of the two algorithms. Colouring asks "which value is least worth a register?" and can answer globally because it has the whole graph. Linear scan asks "what is live right now?" and answers locally because that is all it has kept. Neither is wrong; one has more information and pays for it.
Read it asCompare with the colouring result on the same function and the same two registers: it spills %0 and %2 instead. Two spills either way — our hole-free interval model guarantees the counts match — but colouring chose the two cheapest values to keep in memory, while linear scan chose whoever showed up when the registers were full. The difference in *which* values, on a function where one of them is in a loop, is the ten percent.
Why JITs choose it, and what they add back
For an ahead-of-time compiler, allocation time is paid once by a developer. For a JIT it is paid on every invocation of the compiler, while a user is waiting and while the interpreter continues running the slow version. A tenfold difference in allocator speed for a ten percent difference in code quality is an easy trade at the bottom tiers, and an obviously wrong one at the top — which is why tiered systems use different allocators at different tiers.
Nobody ships plain 1999 linear scan, though. The important extension is *interval splitting*: allow a value to occupy a register for part of its life and memory for the rest, rather than making the decision once for the whole interval. That is exactly the pessimism our hole-free model exhibits, and it is what the second-generation linear scan of Traub, Holloway and Smith and the widely-copied HotSpot C1 variant address. Once splitting and lifetime holes are added, linear scan closes most of the gap to colouring and stops being linear in the strict sense, but keeps a much better constant factor.
The other reason to know this algorithm is that it makes an excellent fallback. LLVM's -O0 path uses a fast allocator rather than its greedy one, because at -O0 nobody is asking for good code and everyone is asking for a fast build.
| Graph colouring (Chaitin-Briggs) | Linear scan | |
|---|---|---|
| Data structure | Interference graph, quadratic in simultaneous liveness | A sorted interval list and two small sets |
| Time | Superlinear; plus rebuilds after spilling | Dominated by the sort; one sweep |
| Spill choice | Global: lowest cost by uses per unit of range | Local: whichever active interval ends last |
| Coalescing | Natural — merge non-adjacent nodes | Awkward; needs a separate pass or a hint mechanism |
| Code quality | Better; the baseline everyone compares against | Roughly within ten percent in the original paper, and closer with splitting |
| Used byimplementation | GCC (IRA, descended from it); optimizing AOT compilers generally | HotSpot C1, many JIT baseline tiers, LLVM's fast -O0 path |
| Fails badly when | The function is enormous or machine-generated | A long-lived value is used constantly and gets spilled for ending late |
How it works
The steps, in the order the compiler takes them.
- Compute live intervals and sort them by start point.
- Maintain a free-register set and an active list sorted by end point.
- For each interval in order: expire every active interval whose end precedes this interval's start, returning its register to the free set.
- If a register is free, assign it and insert this interval into the active list in end-point order.
- Otherwise compare against the latest-ending active interval: whichever of the two ends later is spilled, and the other keeps or takes the register.
- Record why: our engine reports either "ends later than the newcomer, so it blocks a register for longer" or "nothing was free and every active value ends earlier".
How it breaks
What the engineer observes when it goes wrong — not what goes wrong internally.
- The expiry test uses
<=where it should use<(or the reverse), a register is returned to the free set one point too early, two live values share it, and a value is silently corrupted. - A value that is used every iteration of a hot loop is spilled because its interval ends late, and the loop is several times slower than the colouring allocator's version of the same code.
- The active list is not kept sorted by end point, the "latest-ending" lookup returns the wrong interval, and the algorithm spills a value that had almost expired.
- Values pinned to specific registers by the ABI are not modelled, so the sweep assigns them elsewhere and arguments arrive in the wrong registers.
- Copies from out-of-SSA are not coalesced — linear scan has no natural way to do it — and the output is full of register-to-register moves that only a peephole pass removes.
When it helps
- JIT baseline and warm tiers, where compilation happens during execution and the compile time is user-visible latency.
- Debug and
-O0builds, where compilation speed is the whole product and code quality is explicitly not. - Very large machine-generated functions, where building an interference graph is the compile-time bottleneck.
- Any allocator you have to write yourself in a weekend. It fits on one page and it works.
When it hurts
- Hot loops with a long-lived, frequently-used value: the "ends last" rule will pick exactly the wrong victim, because ending late and being used often are not the same property.
- Code with many copies, since coalescing does not fall out of the algorithm the way it does from a graph.
- Optimizing ahead-of-time builds, where the compile-time saving buys nothing anyone can perceive.
What it costs
Every one of these is paid by something.
- Dropping the interference graph buys an order of magnitude of compile time and costs the global view — the spill decision becomes positional, so the algorithm cannot tell a hot value from a cold one.
- Spilling the latest-ending interval buys a decision rule that is computable in constant time from the active list, and costs correctness of *judgement*: interval length is a poor proxy for how much a value is worth.
- Adding interval splitting and holes buys most of the quality back and costs the simplicity and the strict linearity that were the reason to choose the algorithm.
What else you could do
What a different compiler or language does instead, and when that is better.
- Graph colouring, when compile time is not the binding constraint — see
[[graph-coloring-allocation]]. - Second-chance binpacking and the extended linear scans (Traub et al., HotSpot C1), which add splitting and holes and sit between the two in both cost and quality.
- Register allocation by trace: allocate well along the hot path and accept whatever happens off it, which suits profile-guided JITs.
- No allocation at all: keep everything in stack slots, which is what a template JIT and an
-O0build effectively do, and let the hardware's store-to-load forwarding absorb some of the cost.
See it for yourself
The flag, dump or tool that shows you this directly.
- LLVM's comparable fast path:
llc -regalloc=fast file.llbeside-regalloc=greedy, and diff both the assembly and the compile time. - Spill counts for each:
llc -regalloc=fast -stats file.ll 2>&1 | grep -i spill, then again withgreedy. - HotSpot's C1 allocator in action: run with
-XX:TieredStopAtLevel=1and compare the generated code with full tiering via-XX:+PrintCompilation. - Ours:
allocateByLinearScaninsrc/compilers/sim/regalloc.ts, and/compilers/registersruns it besideallocateByColoringon the same function so the two assignments can be diffed directly.
Plausible wrong readings
Stated the way a confident engineer states them.
- "Linear scan is a worse algorithm." It is a different point on a curve. At the tier where it is used, the alternative is not better code — it is the interpreter continuing to run while a slower compiler finishes.
- "It spills the least important value." It spills whichever of two candidates ends later. That correlates with unimportance only loosely, which is the algorithm's main weakness.
- "It is linear time." The sort is not, and modern versions with splitting are not linear either. The name describes the sweep, not the complexity.
- "JITs use it because they are simple." They use it because compilation happens while the user waits. The top tiers of the same JITs use much more elaborate allocators.
Misconceptions
The claim, and what is actually true.
Go deeper
The same idea at increasing depth. Stop wherever it stops being useful.
overview
Line up every value by when it first appears. Walk the line handing out registers, and take a register back the moment its value is no longer needed. If someone arrives and nothing is free, look at who is holding a register the longest and evict whichever of the two will be around longer. It is much less clever than the graph approach and it runs in a fraction of the time.
practical
You meet this algorithm as the reason JIT-compiled code is not as fast as ahead-of-time-compiled code even after warm-up at the lower tiers, and as the reason -O0 builds are quick. If you are benchmarking a JIT, make sure you are measuring the tier you think you are: comparing a baseline-tier linear-scan allocation against an optimizing AOT build is comparing two deliberately different points on the compile-time curve.
advanced
The lasting interest in linear scan is what it reveals about the shape of the allocation problem. Poletto and Sarkar's result was that discarding the entire pairwise conflict structure — the thing that makes the problem NP-complete — costs only around ten percent of code quality. That says the difficulty is concentrated in a small number of decisions rather than spread through the graph, which is also why splitting recovers most of the remaining gap: it targets exactly the cases where a single all-or-nothing decision was too coarse. The general lesson transfers: before investing in an exact solver for a hard combinatorial problem, find out how much the exactness is actually worth on real inputs. Here the answer was "about ten percent, for ten times the time", and that number determined the architecture of every JIT built since.
How much this depends on
Nothing in this domain is true of every compiler. These say how much.
fast allocator at -O0 is simpler still and is not linear scan proper. "JITs use linear scan" is true of a specific set of tiers at a specific time, not a general law.If you were asked this in an interview
- Describe linear scan in five sentences, including the spill rule.
- Why does linear scan spill the interval that ends last, and when is that clearly the wrong choice?
- Why would a compiler ship two register allocators?