Memoryhierarchycachedramlatencycost

The Memory Hierarchy

One big fast memory is not buildable at a price anyone would pay, so machines are built as a stack of progressively larger, slower, cheaper memories that pretend to be one. The gaps between the levels are enormous, and nothing in your source code tells you which level you just hit.

▶ 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
Why is memory built as a hierarchy at all, and how big are the gaps between the levels really?
What you wrote
Every read looks identical in source: `x = arr[i]` is one expression with one apparent cost, and the language offers no syntax that distinguishes a fast read from a slow one.
What the hardware does
That value may already be in a register, or in L1, L2 or L3, or in DRAM, or on a device that must be asked across a bus. Those outcomes span roughly six orders of magnitude, and the hardware picks between them without consulting you.
If you believe all reads cost the same, every performance model you build is wrong in the same direction: you will count operations, predict a runtime, and be surprised. Once you know the hierarchy exists, "how many instructions does this run" becomes the less interesting question and "where does the data live" becomes the more interesting one.
SourceCompilerInstructionsFront EndExecutionRegistersCachesMemoryI/OBehavior

Why not just build one big fast memory?

Because speed, capacity and cost pull against each other in the physical design. Fast storage is built from more transistors per bit, sits physically closer to the execution units, and is wired for low latency rather than density. All three of those make it expensive per byte and limit how much of it can fit near the core. Slow storage inverts every one of those trade-offs.

The engineering answer is not to pick a point on that curve but to have all of them at once, arranged so that the small fast ones hold whatever is being used right now. That only works because real programs are not random: they reuse the same data and they touch neighbouring addresses, which is exactly what Spatial Locality and Temporal Locality name. A hierarchy is a bet on that regularity, and it is a bet that pays off for most code and fails badly for some.

The scale below is the single most useful picture in this domain. Read it as ratios, not times. The absolute numbers depend on the chip, the clock, the memory controller, the DIMM and the load on the machine — but the *shape*, and the fact that each step down costs several times the one above, holds broadly across machines you are likely to meet.

Relative cost of reaching data at each level, with a register read as the unit — 1 unit ≈ one register readSIMPLIFIED
Register×1
L1 cache×4
L2 cache×12
L3 / last level×40
DRAM×200
NVMe SSD×300000
Network storage×3000000
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.
RegisterAlready inside the execution path; effectively free
L1 cacheSmall, per-core, and the target you are aiming for
L2 cacheUsually per-core, larger, a few times slower
L3 / last levelTypically shared between cores; the last stop before leaving the chip
DRAMOff-chip. Two orders of magnitude past a register
NVMe SSDA different world; now you are doing I/O, not memory access
Network storageDominated by the network, not the device

The ratios transfer; the numbers do not

GENERALThe structure is common to essentially all contemporary general-purpose CPUs; every specific size, latency and level count differs by vendor, generation and part

It is tempting to memorise a table of nanoseconds. Resist it. Cache sizes, cache latencies and DRAM timings differ between vendors, between generations from the same vendor, between a laptop part and a server part, and between an idle machine and a loaded one. A number you memorise is a number that will be wrong on the next machine you touch.

What does transfer is the structure: there are several levels, each is several times slower than the one above, the last-level cache is usually shared while L1 is not, and leaving the chip for DRAM is a step change rather than an increment. Those facts let you reason correctly about a machine you have never measured — and then you measure, because §224 applies to you as much as to this page.

The table below is the honest version of "what should I actually assume". Note how little is safely universal. This is why the lessons in this module talk about mechanisms and ratios and hand you to The CPU Counts Itself for the specifics of your machine.

What you can assume, and what you have to measure
PropertySafe to assumeMust be measured
Number of cache levelsAt least two; commonly threeWhether there is an L4 or an on-package memory tier
L1 sharingPrivate to a core (often split instruction/data)Whether sibling hardware threads share it — they usually do
Last-level sharingUsually shared across some group of coresWhich cores share which slice, and how that maps to NUMA: Not All Memory Is Equally Far nodes
Relative gapsEach level costs several times the one aboveThe actual multipliers on your part
Line sizeOne fixed size per level, typically 64 bytes todayThe real value — read it from the OS rather than assuming
Replacement policySomething approximating recencyNothing. Vendors do not document it; see Cache Replacement: LRU Is the Idea, Not the Implementation

Where the hierarchy shows up in ordinary code

You do not need to write unusual code to be affected by this. The two loops below perform the same number of additions on the same data, and differ only in the order they visit it. On a row-major array the first walks memory in order; the second strides across it, taking a fresh line for nearly every element and reusing almost nothing.

The instruction counts are effectively identical. The runtimes are not, and the gap widens as the array grows past each cache level — which is the tell that you are looking at a memory effect and not an arithmetic one. That widening is worth internalising: a memory problem gets *relatively worse* with size, while a compute problem scales smoothly.

The point is not that you should always traverse in row order. It is that "same operations, same count" stopped being a sufficient argument the moment the hierarchy existed. When two implementations with equal complexity perform differently, the memory hierarchy is the first place to look, and Busy Is Not the Same as Working is how you confirm it.

Column-major traversal of a row-major array
1for (col = 0; col < N; col++)
2 for (row = 0; row < N; row++)
3 sum += a[row][col];
4
5// Each step jumps N elements forward in memory.
6// Once N is large, every access is a different line.
7// The line that arrives brings 15 neighbours you never use.
Row-major traversal of a row-major array
1for (row = 0; row < N; row++)
2 for (col = 0; col < N; col++)
3 sum += a[row][col];
4
5// Consecutive steps are adjacent in memory.
6// One line fetch serves many iterations.
7// The prefetcher recognises the stride and runs ahead.

Identical arithmetic, identical instruction count, different memory order. The first version pays a fetch per element; the second amortises one fetch across everything sharing the line, and lets Prefetching: The Hardware Guesses What You Will Read Next hide even that. This is the whole module in one example.

Key points

  • The hierarchy exists because fast, large and cheap cannot be satisfied at once — so machines have all of them and move data between them.
  • The gaps are large: a DRAM access costs on the order of a hundred times an L1 hit, and leaving the chip is a step change rather than an increment.
  • Ratios and structure transfer between machines; absolute latencies and sizes do not, and memorising them will mislead you.
  • Nothing in your source distinguishes a register hit from a DRAM trip — which is exactly why memory effects feel like magic.
  • A performance gap that widens as the data grows is a memory-hierarchy signature, not an algorithmic one.

Progressive depth

Overview

Machines have several memories of increasing size and decreasing speed, arranged so the small fast ones hold what is being used now. Reading arr[i] might be answered by any of them, and the difference between the best and worst case is enormous.

Practical

Assume roughly: L1 a few times a register, L2 a few times L1, L3 a few times L2, and DRAM a large multiple again. Aim to keep hot data small enough to stay in a level, and traverse it in layout order. When two equal-complexity implementations differ in speed, check placement before arithmetic.

Advanced

The hierarchy is not a simple ladder. Lines are installed at multiple levels on the way back, caches may be inclusive or exclusive of one another, the last level is shared and therefore contended, and several misses can be in flight simultaneously so their latencies partly overlap (Misses That Overlap Are Nearly Free). A single "DRAM latency" number ignores all of this.

Internals

Each level maintains tags, state bits and a replacement approximation; misses allocate line-fill buffers that themselves limit outstanding requests; the memory controller reorders and batches requests to respect DRAM row activation and refresh timing. Coherence traffic from other cores shares the same interconnect, so a "memory" cost is partly a *sharing* cost — which is why Cache Coherence: Why Shared Memory Works At All belongs to performance and not only to correctness.

Where the Data Is

Change an input and watch which number moves — and which one refuses to.

Where a value can be, and roughly what each costs relative to a register — 1 unit ≈ one register accessSIMPLIFIED
Register×1
L1 cache×4
L2 cache×14
L3 cache×45
DRAM×200
NVMe storage×100000
Network round trip×10000000
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.
RegisterAlready in the core. Effectively free.
L3 cacheUsually shared between cores, so other work affects your hit rate.
DRAMTwo orders of magnitude past L1. This is the cliff.
NVMe storageAnother three orders of magnitude, and the OS gets involved.
Network round tripDifferent universe. Included to keep the earlier rows in perspective.

The exact ratios vary by machine and the absolute times vary far more, which is why none are shown. What is stable enough to build intuition on is the shape: each level is several times the one above, and the gap between the last cache level and memory is the one that decides most program performance.

Follow the mechanism

The path through the machine, hop by hop — and the conclusions it invites that are wrong.

  1. 1
    Core → L1: the load unit presents an address; on a hit the data returns in a handful of cycles and execution continues.
  2. 2
    L1 → L2: on a miss L1 forwards the request to L2, and the requesting instruction waits unless the core can find independent work.
  3. 3
    L2 → L3: another miss walks out to the shared last-level cache, now competing with other cores for capacity and bandwidth.
  4. 4
    L3 → memory controller: a last-level miss leaves the chip. The controller queues the request against DRAM timing constraints.
  5. 5
    DRAM → core: the line is returned and installed at each level on the way back, so the next access to any byte in it is a hit.
What people conclude from this — wrongly
  • "L1 is faster than RAM" stated as the whole lesson — true, uninformative, and it does not tell you what to change.
  • Treating a published latency table as applying to your machine, then building a cost model on numbers that are wrong by a factor of two.
  • Concluding a loop is compute-bound because the CPU shows 100% utilisation; a core stalled on memory is still counted as busy (Busy Is Not the Same as Working).
  • Assuming more cache always helps — a streaming workload with no reuse gets nothing from a larger cache (Three Kinds of Miss, Three Different Fixes).

Consequences, controls and cost

What it causes
  • • Programs with equal instruction counts can differ by an order of magnitude in runtime purely through data placement.
  • • Performance degrades in steps rather than smoothly as a working set grows past each cache level.
  • • Optimising arithmetic in a memory-bound loop produces no measurable improvement, which reads as "the optimisation did nothing".
  • • Benchmarks that fit in cache report numbers the production workload will never see — see [[microbenchmarking-pitfalls]].
What you can do
  • • Shrink the working set so more of it fits in a level — smaller types, fewer fields, denser encodings ([[working-set]]).
  • • Traverse in the order the data is laid out, so each fetched line is fully used ([[spatial-locality]]).
  • • Restructure the algorithm to reuse data while it is still resident, rather than streaming past it repeatedly ([[matrix-tiling]]).
  • • Choose layouts that put the fields you actually touch together ([[aos-vs-soa]]).
  • • Measure which level you are missing in before optimising for any of them — the fix differs per level.
How to see it
  • • Read the actual cache sizes and line size from the OS rather than assuming: on Linux, `lscpu` or `/sys/devices/system/cpu/cpu0/cache/`.
  • • Sweep a working set across sizes and plot time per element; the cliffs mark your cache capacities on this machine.
  • • Sample cache-miss counters per level and compare miss counts against loads retired to get a miss rate, not just a count.
  • • Compare the same benchmark at a size that fits in L2 against one that does not — the ratio is your local penalty.
  • • Watch whether the gap between two implementations widens with input size: widening implicates memory, flat implicates arithmetic.
What it costs
  • • Restructuring for locality usually costs readability, and sometimes costs an abstraction the rest of the codebase relies on.
  • • Shrinking data to fit a level can mean lossy encodings, more unpacking work, or types that are awkward in the source language.
  • • Cache-conscious layouts are tuned to a machine profile; the tuning can be wrong on the next generation, so it needs re-measuring rather than trusting.

Scope

§224 — what these claims are specific to.

What these claims are specific to
  • GENERALThe multi-level structure holds across essentially all contemporary general-purpose CPUs; the level count, sizes and inclusivity policy differ by vendor and generation
  • SIMPLIFIEDThe cost scale omits store buffers, line-fill buffers, memory-level parallelism and the fact that several misses can be outstanding at once — see Misses That Overlap Are Nearly Free

Misconceptions

Claim
“Cache is just faster RAM.”
Reality
It is a different mechanism, not a faster instance of the same one. RAM is addressed directly; a cache is a hardware-managed, tagged, associative store that holds *copies* of lines and must decide what to keep and what to evict. That decision machinery is the whole subject of Tag, Index and Offset: How an Address Finds Its Line and Cache Replacement: LRU Is the Idea, Not the Implementation, and RAM has no equivalent.
Claim
“RAM access time is constant.”
Reality
It varies with row-buffer state inside the DRAM, with contention from other cores, with which NUMA: Not All Memory Is Equally Far node owns the address, and with how many requests are already queued at the controller. "Constant-time random access" is a programming model, not a hardware property — see How DRAM Is Organised.
Claim
“If my data fits in RAM I do not need to think about the hierarchy.”
Reality
Fitting in RAM only rules out the *storage* cliff. The gaps between registers, L1, L2, L3 and DRAM are where most real programs lose their time, and all of them are inside "fits in RAM".

Apply it

Where the rest of this lives

Programming Languages & Runtime Internals
Object layout and allocator behaviour

Which level answers a read is decided largely by how the runtime laid the object out and where the allocator put it — neither of which appears in your source.