CPUregistersregister filespillingregister pressurearchitectural state

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.

▶ Run the labFollow the mechanism

Software view, hardware view

The gap between what you wrote and what the machine does is where this whole domain lives.

The question
Where do the values my arithmetic operates on actually live, and what happens when there is not enough room for them?
What you wrote
I declare local variables and use them. Where they live is the compiler's problem.
What the hardware does
Arithmetic units read and write a small register file. Values that do not fit are spilled to the stack — which is memory, and therefore the cache hierarchy, and therefore hundreds of times more expensive on a miss.
Register pressure is the difference between a loop whose working values stay in the core and one that touches memory on every iteration. It is invisible in source and visible immediately in the generated assembly.
SourceCompilerInstructionsFront EndExecutionRegistersCachesMemoryI/OBehavior

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.

Where a value can live, relative to a register access — 1 unit ≈ one register readMICROARCH-SPECIFIC
Register×1
Spilled to stack, hits L1×4
Hits L2×14
Hits last-level cache×40
Goes to DRAM×200
Ratios, not times. Absolute latencies depend on the processor, its clock, the memory it is attached to and what else is running — publishing them would be wrong everywhere except one machine. The bars are log-scaled, so each step is larger than it looks.
RegisterDirectly readable by the execution units
Spilled to stack, hits L1The common case for spills — the stack is hot
Hits L2If the stack frame has been displaced
Goes to DRAMRare for stack data, catastrophic in a loop

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.

Two different things that are both called "a register"
Architectural registerPhysical register
Defined byThe ISA — part of the software/hardware contractThe microarchitecture — an implementation choice
How manyA fixed, small number named by instruction encodingTypically many more; the count is a design decision
Visible to softwareYes, by name, in every instructionNo, never named directly
Changes between CPU generationsNo — that would break every binaryFreely, and it routinely does
Why it existsTo give compilers a stable targetTo 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).

Many simultaneously live values — the allocator runs out and spills
1// 12 accumulators live across the whole inner loop
2for i in 0..n:
3 a0 += x[i]*w0; a1 += x[i]*w1
4 a2 += x[i]*w2; a3 += x[i]*w3
5 a4 += x[i]*w4; a5 += x[i]*w5
6 a6 += x[i]*w6; a7 += x[i]*w7
7 a8 += x[i]*w8; a9 += x[i]*w9
8 a10 += x[i]*w10; a11 += x[i]*w11
9
10// inner loop now contains stack traffic:
11// store [rsp+0x18], reg
12// load reg, [rsp+0x20]
13// ...on every iteration
Blocked so that live values fit — accumulators stay in registers
1// process weights in blocks that fit the register budget
2for block in chunks(weights, 4):
3 b0 = b1 = b2 = b3 = 0
4 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 iterations

Neither 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.

A four-register machine
R15
R27
R30
R40
Executed
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.

  1. 1
    Compiler → allocator: local values are assigned to architectural registers for the region in which they are live.
  2. 2
    Allocator → spill code: when simultaneously live values exceed the register count, some are stored to the stack and reloaded on use.
  3. 3
    Instruction → decode: the architectural register names encoded in the instruction are read out.
  4. 4
    Decode → rename: each write is mapped to a fresh physical register, removing false dependencies on the reused name.
  5. 5
    Execution unit → physical register file: operands are read and results written; retirement makes the mapping architecturally visible.
What people conclude from this — wrongly
  • "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

What it causes
  • • 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.
What you can do
  • • 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.
How to see it
  • • 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.
What it costs
  • • 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.

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

Claim
“Registers are just a very small, very fast cache.”
Reality
A cache is addressed by memory address and managed by hardware; registers are named directly in the instruction encoding and managed by the compiler. The difference matters: you can control register allocation by restructuring code, and you cannot control cache placement the same way.
Claim
“Declaring fewer local variables reduces register pressure.”
Reality
Pressure is determined by how many values are simultaneously *live* after inlining and optimisation, not by how many names appear in source. Restructuring to shorten live ranges works; renaming variables does not.
Claim
“The CPU has as many registers as the ISA names.”
Reality
It typically has considerably more physical registers, and renames architectural names onto them. That surplus is what makes out-of-order execution possible.

Where the rest of this lives

Programming Languages & Runtime Internals
Register allocation

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.