Placement is the phase that gives every parameter, local, and temporary a backend-visible home — a physical register, or a slot in the activation record. Everything downstream reads that decision and nothing re-litigates it.

A debug build has no optimised-away variables

Register homing is a release-mode optimisation and nothing else. When the debug profile is set, no allocator runs at all: every value keeps its stack home for the whole function. The checked profile implies debug and behaves the same way.

The consequence is the one that matters at a breakpoint: each value has a single stable location the debugger can read at any program point, not just where an allocator happened to leave it in memory. Placement writes each parameter and variable home into the DWARF v5 record, so the location the debugger is told about is the location the code uses. You never step into a frame and find a variable that exists in the source and nowhere in the binary.

This is a trade: a debug build is materially slower precisely because no value keeps a register home.

The default allocator: a whole-function linear scan

On the release path the default is a linear scan over unit-wide live intervals — whole-function, not per-basic-block. A value may keep its register home across block boundaries and across calls, because the pool is anchored on callee-saved registers and so survives both. The scan runs once per register class, each class drawing from its own pool.

The scan never splits a live range: a value keeps one home for its entire lifetime. That is what keeps merge points free of edge moves — a variable assigned on several incoming paths targets the same register on every path, so the merge needs no parallel copies.

Graph colouring with copy coalescing

The second allocator colours the interference graph by the Chaitin–Briggs method: the physical registers of one class are the colours, two simultaneously-live values may not share one, and the two-phase simplify/select pass with optimistic spilling finds an assignment. Colouring runs once per class, since distinct classes never share a register. Before colouring, move-related pairs are coalesced, so a copy’s two ends take the same register and the move between them disappears.

Admission rules ride along as per-node allowed-colour masks rather than as a caller-saved post-pass: a caller-saved register dies at a call and is masked out for any call-crossing value; an argument-staging register is additionally masked out for values crossing an argument point or bound from the function entry. The one exception is the scratch-shared registers. Their availability is a per-point capacity question no static mask can express, so they stay out of the colouring pool and are handled by a demand-capped pass afterwards.

The reason the pool is the full one

Colouring against the callee-saved registers alone was tried first, and it starved the floating-point class. The System V ABI on x86_64 preserves no floating-point register across a call, so on that target the class had nothing to colour against. On AArch64 the callee-saved band left it a pool small enough that Briggs’s conservative test declined nearly every merge in a high-pressure float recurrence, and a separate pass then homed the remainder with no coalescing awareness. Measured against the kernel below — every floating-point local held live across two reductions — that arrangement cost a handful of extra fmov shuffles.

    { each pairwise product reads two live floats; the sum keeps all fourteen live at once }
    products := a * b + c * d + e * f + g * h + i * j + k * l + m * n;
    WriteLn("products = %.2lf", products);

    { a second reduction reads all fourteen again, so they stay live past the first use }
    total := a + b + c + d + e + f + g + h + i + j + k + l + m + n;

Against the full pool the same merges pass. That is a historical measurement of a superseded design on one kernel, not a live compiler statistic.

Spill costs follow loop depth

An allocator that evicts by interval length is loop-blind: an outer loop’s index has the longest interval in its function and is evicted first, though it is read on every iteration of everything the loop contains. Mica weighs each value by its operand occurrences, multiplied by an order-of-magnitude factor for each enclosing loop and saturating at a bounded nesting depth. Eviction compares weights rather than interval ends. Where no loop nesting is available the weights degrade cleanly to plain use counts.

The result is checked, not trusted

Every allocation behind the placement seam passes an always-on correctness net that re-derives interference independently and rejects any two interfering values sharing a physical register — other than the coalesced pairs, which share a value by construction. A third strategy exists purely to keep the seam honest — a deliberately different but still valid allocation — so a change that works only for one allocator’s register choices is caught rather than shipped. Across the corpus of 1,389 test programs both paths are exercised — release configurations through the linear scan, the O2 configurations through the colourer — and a single switch re-runs every release configuration in the suite through the colourer on demand.

See also