The memory hierarchy

Programmers want memory that is huge, fast and cheap. No single technology is all three, so computers stack several, the fast small ones close to the processor:

LevelTypical sizeTypical timeNote
Registers~1 KBunder 1 nsinside the CPU
L1 cache32–64 KB~1 nsSRAM, per core
L2 cache256 KB–2 MB~4 nsSRAM
L3 cache8–64 MB~15 nsshared by the cores
Main memory8–64 GB~80 nsDRAM
SSD0.5–4 TB~100 µsflash; keeps data without power
Hard disk1–20 TB~10 msspinning

From L1 to a disk, the time grows by a factor of ten million. If each L1 access were one second, a disk access would take almost four months.

Locality

Caches work because programs are predictable:

  • Temporal locality: data used now is likely to be used again soon (a loop counter, a running total).
  • Spatial locality: data near something just used is likely to be used soon (the next array element, the next instruction).

So each level keeps recently used data, and moves it in blocks (often 64 bytes), not single bytes. The order in which a program walks through an array matters a lot:

rows, cols, block = 4, 4, 4           # a 4 x 4 array stored row by row; 4 elements per cache block

def block_loads(by_rows):
    held, loads = None, 0                # a tiny cache that keeps one block
    for i in range(rows * cols):
        r, c = (i // cols, i % cols) if by_rows else (i % rows, i // rows)
        b = (r * cols + c) // block
        if b != held:
            loads += 1
            held = b
    return loads

print("row by row:", block_loads(True), "block loads")
print("column by column:", block_loads(False), "block loads")
Open in compiler

Average memory access time

AMAT = hit time + miss rate × miss penalty

With two levels of cache, the L1 miss penalty is itself the L2's AMAT:

l1_hit, l1_miss = 1, 0.05        # ns, fraction of accesses
l2_hit, l2_miss = 10, 0.20       # of the accesses that reach L2
memory = 100

l2_amat = l2_hit + l2_miss * memory
amat = l1_hit + l1_miss * l2_amat
print("L2 average time:", l2_amat, "ns")
print("overall AMAT:", round(amat, 2), "ns")
print("without L2:", l1_hit + l1_miss * memory, "ns")
Open in compiler

A small change in miss rate makes a big difference, because the miss penalty is so large. That is why caches are a central topic in processor design (next lesson).

Common mistakes

  • Thinking a bigger memory is always faster. Bigger means slower; the hierarchy hides it.
  • Adding miss penalties for every level for every access: the L2 time counts only for L1 misses.
  • Walking a 2-D array column by column in a language that stores it row by row.

Quick check

1. A loop adds up the elements of an array in order. Which locality does it mainly use?
2. Hit time 1 ns, miss rate 10%, miss penalty 50 ns. What is the AMAT?
3. Which level is fastest?
4. Why are caches loaded in blocks of many bytes?

Practice problems for this lesson

My notes

Write down what you want to remember from this lesson. Notes are saved on this device as you type, and appear on your dashboard.