Registers: The Fastest Storage, and There Is Almost None of It
Registers are the only storage the ALU can read directly, they are the fastest thing in the machine by a wide margin, and an ISA typically exposes a couple of dozen of them. Everything a compiler does with local variables is an attempt to keep the right values in that tiny space.
Software view, hardware view
The gap between what you wrote and what the machine does is where this whole domain lives.
Fast, and very small
A register access is the cheapest data access the machine has — cheap enough that in most models it is the unit everything else is quoted against. What makes it cheap is exactly what makes it scarce: registers sit inside the core, adjacent to the execution units, addressed by a handful of bits encoded directly in the instruction. Making the file bigger costs area, power and access time, and widens every instruction that names a register.
So an ISA exposes few of them. The exact count is an ISA property and differs substantially — this is one of the more visible differences between instruction sets, and one of the few that genuinely affects generated code quality. What matters for reasoning is the order of magnitude: tens, not thousands. Every local variable, loop counter, array base pointer and intermediate result in a hot loop is competing for that space.
The cost ratio below is the reason the competition matters. A value in a register is available essentially immediately. The same value spilled to the stack is a memory access — it will usually hit L1, because the stack is hot, but "usually hits L1" is still an order of magnitude worse than "is already here", and it consumes a load/store slot that some other operation wanted.
The registers the ISA names are not the registers the machine has
There is a second, deeper distinction hiding here. The registers an instruction can name — the architectural registers — are part of the ISA contract. The registers the hardware actually stores values in — the physical registers — are a microarchitectural implementation detail, and there are typically far more of them.
The reason is subtle and worth carrying: reusing an architectural register name creates a *false* dependency. If one instruction writes r1 and a later, entirely unrelated instruction also writes r1, the machine must not let the second overwrite the first before earlier readers have finished — even though the two computations have nothing to do with each other. Mapping each write onto a fresh physical register removes that constraint, which is what Register Renaming does and why out-of-order execution is possible at all.
The practical consequence for a programmer is mostly reassurance: reusing a variable name for an unrelated purpose does not create a hardware dependency, because the machine renamed it away. The practical consequence for reading assembly is that register numbers in a disassembly are architectural names, not storage locations, and counting them tells you about the ISA rather than about the machine.
| Architectural register | Physical register | |
|---|---|---|
| Defined by | The ISA — part of the software/hardware contract | The microarchitecture — an implementation choice |
| How many | A fixed, small number named by instruction encoding | Typically many more; the count is a design decision |
| Visible to software | Yes, by name, in every instruction | No, never named directly |
| Changes between CPU generations | No — that would break every binary | Freely, and it routinely does |
| Why it exists | To give compilers a stable target | To break false dependencies so execution can overlap |
Register pressure and spilling, in the generated code
When a region of code needs more simultaneously live values than there are registers, the compiler must spill: store some value to the stack and reload it later. This is not a failure — it is the correct thing to do — but it converts a free access into a memory operation inside whatever loop it happens in.
The comparison below is the classic shape. Two loops compute the same thing; one keeps its accumulators in registers because there are few enough of them, the other carries so many simultaneously live values that the compiler runs out and starts spilling in the inner loop. The arithmetic is identical. The generated code is not, and neither is the runtime.
The honest caveat is that you cannot reliably predict this from source. Whether a given loop spills depends on the target ISA's register count, the compiler's allocator, the optimisation level, and what else got inlined into the function. The reliable method is to look: compile it and read the assembly for stack traffic in the inner loop. That is a two-minute check and it converts a guess into a fact (Reading Assembly Without Writing It).
1// 12 accumulators live across the whole inner loop2for i in 0..n:3 a0 += x[i]*w0; a1 += x[i]*w14 a2 += x[i]*w2; a3 += x[i]*w35 a4 += x[i]*w4; a5 += x[i]*w56 a6 += x[i]*w6; a7 += x[i]*w77 a8 += x[i]*w8; a9 += x[i]*w98 a10 += x[i]*w10; a11 += x[i]*w119 10// inner loop now contains stack traffic:11// store [rsp+0x18], reg12// load reg, [rsp+0x20]13// ...on every iteration1// process weights in blocks that fit the register budget2for block in chunks(weights, 4):3 b0 = b1 = b2 = b3 = 04 for i in 0..n:5 b0 += x[i]*block[0]6 b1 += x[i]*block[1]7 b2 += x[i]*block[2]8 b3 += x[i]*block[3]9 commit(b0, b1, b2, b3)10 11// inner loop is now register-only:12// no stack loads or stores between iterationsNeither version performs different arithmetic. The first keeps more values live simultaneously than the register file can hold, so the compiler inserts a store and a reload into the hottest loop in the program. Blocking reduces the number of live values below the budget, and the memory traffic disappears — the same restructuring idea as Matrix Tiling: Same Arithmetic, Ten Times Faster, applied to registers instead of cache.
Key points
- Registers are the only storage arithmetic units read directly, and an ISA exposes tens of them, not thousands.
- A spill turns a free access into a memory access — usually an L1 hit, but never free, and it occupies a load/store slot.
- Architectural registers are an ISA contract; physical registers are an implementation detail, and there are many more of the latter.
- Renaming means reusing a variable name does not create a hardware dependency, which is what allows execution to overlap.
- You cannot predict spilling from source — read the generated assembly for stack traffic in the inner loop.
Register File
Change an input and watch which number moves — and which one refuses to.
Run an instruction.
Arithmetic happens between registers, never directly on memory operands on most architectures. Getting data into a register is a separate load instruction — and that load is where nearly all the time goes.
Follow the mechanism
The path through the machine, hop by hop — and the conclusions it invites that are wrong.
- 1Compiler → allocator: local values are assigned to architectural registers for the region in which they are live.
- 2Allocator → spill code: when simultaneously live values exceed the register count, some are stored to the stack and reloaded on use.
- 3Instruction → decode: the architectural register names encoded in the instruction are read out.
- 4Decode → rename: each write is mapped to a fresh physical register, removing false dependencies on the reused name.
- 5Execution unit → physical register file: operands are read and results written; retirement makes the mapping architecturally visible.
- • "Fewer variables in my source means fewer registers used." The compiler decides liveness after inlining and optimisation; source-level variable count is almost unrelated.
- • "Spilling means the compiler failed." Spilling is correct behaviour when live values exceed the register file. The question is whether it happens in a hot loop.
- • "Register numbers in a disassembly tell me how much storage the CPU is using." They are architectural names; the machine renamed them onto a much larger physical file.
Consequences, controls and cost
- • A hot loop that spills performs stack loads and stores on every iteration, none of which appear anywhere in the source.
- • Aggressive inlining can increase register pressure in the caller and introduce spills that were not there before.
- • Code compiled for an ISA with more architectural registers may generate materially better inner loops from identical source.
- • Reduce the number of simultaneously live values in hot loops — block or chunk the work so accumulators fit.
- • Read the disassembly of the inner loop and look for stack traffic; this is the only reliable detection method.
- • Be sceptical of inlining hints in already register-hungry code; the win from removing a call can be undone by spills.
- • Otherwise leave it alone — register allocation is one of the things compilers genuinely do better than hand-tuning.
- • Compile with optimisation and disassemble the hot function; count stack loads and stores inside the loop body.
- • Compare generated code across optimisation levels — spills appearing at higher levels usually indicate aggressive inlining raising pressure.
- • On platforms that expose it, watch store-forwarding and load counters; a spill-heavy loop shows elevated load/store unit activity relative to arithmetic.
- • Blocking to reduce register pressure makes code longer and less obviously correct than the straightforward version.
- • Restructuring for the register budget targets one ISA's register count and may be pointless or harmful on another.
- • Time spent on register pressure is wasted unless the loop is genuinely hot and genuinely not memory-bound.
Scope
§224 — what these claims are specific to.
- ISA-SPECIFICThe number of architectural registers is defined by the ISA and varies substantially between instruction sets; generated code quality for register-hungry loops varies with it.
- MICROARCH-SPECIFICPhysical register file size, renaming capacity and spill cost are implementation details that differ between generations and vendors.
Misconceptions
Where the rest of this lives
Which values land in registers is a compiler decision made by a graph-colouring or linear-scan allocator. The hardware supplies the file; the compiler decides who gets it.