CMP-310 · Lesson 7 of 12 · 19 min

The memory hierarchy and cache design

A processor that can start several instructions per nanosecond is useless if it waits a hundred nanoseconds for each value from main memory. Processor speeds rose far faster than memory speeds for decades, opening a gap often called the memory wall, and the memory hierarchy is the architect's answer. Small, fast memories near the processor hold copies of the data most likely to be needed next; larger, slower, cheaper memories further away hold everything else. If the guesses are good, the processor sees nearly the speed of the smallest memory and nearly the capacity of the largest. This lesson covers how caches are organized and the choices that determine how good their guesses are.

CMP-229 introduced the direct-mapped cache. The NS-5 candidates go further: Core A has 8 KiB instruction and 8 KiB data caches, both two-way set-associative, while Core B has 16 KiB caches of each kind, four-way set-associative, and both sit in front of a shared external memory. Jonah's task this week is to understand why the radar filter runs almost twice as fast on one cache configuration as on another, even though the code is identical.

Locality: why caches work

Caches work because programs exhibit locality. Temporal locality means that a recently accessed item is likely to be accessed again soon: loop counters, the instructions of a loop body, the coefficients of a filter. Spatial locality means that items near a recently accessed one are likely to be accessed soon: the next element of an array, the next instruction, the next field of a structure. Caches exploit temporal locality by keeping recently used data and spatial locality by moving data in blocks, also called lines, of 32 or 64 bytes, so that one miss brings in neighbours that will probably be needed next.

Typical levels of the memory hierarchy in a desktop or server processor. Embedded parts like the NS-5 candidates have only one cache level and much smaller, slower memories.
LevelTypical sizeTypical latencyBuilt from
RegistersHundreds of bytesPart of a cycleFlip-flops in the core
Level 1 cache16 to 64 KiB per coreA few cyclesSRAM
Level 2 cache256 KiB to a few MiBAbout 10 to 20 cyclesSRAM
Level 3 cacheSeveral to tens of MiB, sharedAbout 30 to 60 cyclesSRAM
Main memoryGigabytesAbout 50 to 100 ns, hundreds of cyclesDRAM
Flash storageHundreds of gigabytes or moreTens of microsecondsNAND flash
A pyramid of memory levels from registers at the narrow top through level 1, level 2 and level 3 caches to main memory and flash storage at the wide base. Arrows on the left show capacity increasing downward and on the right show latency and cost per byte decreasing in speed downward, with approximate latencies labelled at each level.
The memory hierarchy. Each level is larger, slower and cheaper per byte than the one above it.

Cache organization

A cache is described by its capacity, its block size and its associativity. The block is the unit of transfer. Associativity is the number of places in the cache where a given block may be stored. In a direct-mapped cache each block has exactly one possible location; in an n-way set-associative cache the cache is divided into sets of n blocks and a block may go in any of the n ways of one particular set; in a fully associative cache a block may go anywhere. The set is chosen by some bits of the address, the remaining high bits are stored as a tag to identify which block occupies each way, and the low bits select the byte within the block.

\[\text{sets} = \frac{\text{capacity}}{\text{block size} \times \text{ways}}, \quad b_{\text{offset}} = \log_2(\text{block size}), \quad b_{\text{index}} = \log_2(\text{sets}), \quad b_{\text{tag}} = n_{\text{address}} - b_{\text{index}} - b_{\text{offset}}\]

On an access, the cache uses the index bits to select a set, reads the tags of all ways in that set in parallel, and compares them with the address tag. A match with the valid bit set is a hit, and the data are delivered from that way. With no match, the access is a miss: the block is fetched from the next level and placed in one of the set's ways, evicting whatever was there. Higher associativity means more comparators and a wider multiplexer, which costs area, energy and sometimes hit time, but it reduces misses caused by blocks competing for the same set.

A 32-bit address split into a 20-bit tag, a 7-bit index and a 5-bit block offset. The index selects one row in a two-way set-associative cache drawn as two side-by-side arrays of tag, valid bit and data. Two comparators compare the stored tags with the address tag, and their outputs drive a hit signal and a multiplexer that selects the data from the matching way.
Looking up an address in a two-way set-associative cache. The index picks a set, both tags are compared at once, and the matching way supplies the data.

Worked problem

Splitting an address

Core B's data cache holds 16 KiB in 32-byte blocks and is four-way set-associative; addresses are 32 bits. How many sets does it have, and how are the address bits divided? In which set is address 0x00011A44 placed?

Show solution
  1. \[ \text{sets} = \frac{16384}{32 \times 4} = 128\]
  2. The offset needs log₂ 32 = 5 bits, the index log₂ 128 = 7 bits, and the tag the remaining 32 − 7 − 5 = 20 bits.
  3. 0x00011A44 shifted right by 5 bits is 0x8D2; its low 7 bits, 0x52, select set 82, and the byte offset is 0x04.

Answer: The address lands in set 82 at byte 4 of its block, and the cache stores the upper 20 bits, 0x00011, as the tag.

Replacement policies

When a miss occurs in a set-associative cache whose set is full, one way must be evicted. Least recently used replacement evicts the block that has gone longest without an access, on the theory that temporal locality makes it the least likely to be needed. True LRU needs to track the order of use within each set, which is cheap for two ways but costly for eight or sixteen, so designs approximate it with pseudo-LRU trees or similar schemes. Random replacement is simpler and, for large associativity, performs nearly as well. Modern last-level caches use adaptive policies that detect streaming access patterns, data used once and never again, and insert such blocks at low priority so they do not flush out useful data.

Write policies

Writes raise two questions. When a store hits, is the next level updated at once? A write-through cache sends every write to the next level, usually through a write buffer so the processor need not wait; it keeps the levels consistent and is simple, but generates a lot of traffic. A write-back cache updates only its own copy and marks the block dirty, writing it to the next level only when the block is evicted; it saves bandwidth and energy when a block is written repeatedly, which is common. When a store misses, is the block brought into the cache? Write-allocate fetches the block and then writes it, which suits write-back caches and code that soon reads what it wrote; no-write-allocate sends the write onward without filling the cache, which suits write-through caches and streaming output.

For the NS-5, write-back matters for energy, because every transaction on the external memory bus costs far more energy than an access to on-chip SRAM. Core A's data cache is write-through with a four-entry write buffer; Core B's is write-back. The filter writes each output sample once per frame, so the difference shows up mainly in the energy measurements of lesson 12.

Classifying misses: the three Cs

Misses fall into three categories. Compulsory misses occur on the first access to a block, which cannot be in the cache yet; larger blocks and prefetching reduce them. Capacity misses occur because the cache cannot hold all the blocks a program uses in a period; only a larger cache, or a program that touches less data at a time, reduces them. Conflict misses occur in direct-mapped and set-associative caches when too many active blocks map to the same set, so that blocks are evicted even though the cache as a whole has room; higher associativity reduces them. A fourth C, coherence misses, appears in multiprocessors, where another core's write invalidates a block, as lesson 10 explains. Classifying misses this way tells the designer which change will help.

Jonah simulates the radar filter's data accesses. The filter computes y[i] += c[i mod 64] × x[i] over frames of 2,048 samples, and the linker has placed the three arrays exactly 4 KiB apart, a common result of aligning large arrays to page boundaries. The program compares a 4 KiB data cache with 32-byte blocks at several associativities.

Hit rates for the filter loop in a 4 KiB data cache at different associativities.
# A set-associative cache simulator with LRU replacement
def simulate(trace, size=4096, block=32, ways=1):
    sets = size // (block * ways)
    cache = [[] for _ in range(sets)]          # each set: list of tags, most recent last
    hits = 0
    for addr in trace:
        blk = addr // block
        idx, tag = blk % sets, blk // sets
        s = cache[idx]
        if tag in s:
            hits += 1; s.remove(tag)
        elif len(s) == ways:
            s.pop(0)                            # evict least recently used
        s.append(tag)
    return hits / len(trace)

# Filter: y[i] += c[i % 64] * x[i] over 2048-sample frames; arrays placed 4 KiB apart
X, Y, C = 0x10000, 0x11000, 0x12000
trace = []
for frame in range(4):
    for i in range(2048):
        trace += [C + 4 * (i % 64), X + 4 * i, Y + 4 * i, Y + 4 * i]   # load c, load x, load y, store y

print("4 KiB, 32-byte blocks, direct mapped: offset 5 bits, index 7 bits (128 sets), tag 20 bits")
for ways in (1, 2, 4, 128):
    label = "fully associative" if ways == 128 else f"{ways}-way"
    print(f"{label:18s} hit rate {simulate(trace, ways=ways):.3f}")
Output
4 KiB, 32-byte blocks, direct mapped: offset 5 bits, index 7 bits (128 sets), tag 20 bits
1-way              hit rate 0.507
2-way              hit rate 0.874
4-way              hit rate 0.938
fully associative  hit rate 0.938

The direct-mapped cache hits only about half the time. Because the arrays are 4 KiB apart and the cache is 4 KiB, element i of each array maps to the same set, so loading c evicts y, loading x evicts c, and so on: almost every access is a conflict miss. Two ways help a lot, raising the hit rate to 87 percent, but three arrays still compete for two ways in the sets that hold coefficients. Four ways remove the conflicts entirely: the hit rate of 93.8 percent equals that of a fully associative cache, and the remaining misses are compulsory, one per 32-byte block of x and of y. The same effect explains the near-doubling of speed Jonah saw: a two-line change to the linker script that offsets the arrays by 96 bytes lifts the direct-mapped hit rate to about 94.7 percent in the same simulation, as good as the four-way cache.

Average memory access time

The standard measure of a cache's effect on performance combines hit time, miss rate and miss penalty. Average memory access time, AMAT, is the hit time plus the fraction of accesses that miss times the time to service a miss from the next level. Lesson 8 extends it to several levels and uses it to evaluate cache optimizations.

\[\text{AMAT} = t_{\text{hit}} + m \times t_{\text{penalty}}\]

With a hit time of 1 cycle and a miss penalty of 40 cycles, the direct-mapped filter's miss rate of about 0.49 gives an AMAT of about 20.7 cycles, while the four-way cache's miss rate of 0.062 gives about 3.5 cycles. Both numbers swamp the hit time, which is why miss rate, not hit time, dominates cache design for most workloads.

Instruction caches and split versus unified designs

Instruction fetch has excellent locality, because loops execute the same instructions repeatedly and straight-line code is sequential. Separate level 1 instruction and data caches let the pipeline fetch and load in the same cycle, avoiding the structural hazard a single cache would create, and each can be tuned to its access pattern. Lower levels are usually unified, holding both instructions and data, so that capacity is shared according to demand. Instruction caches are read-only from the core's point of view, which simplifies them: they never hold dirty data.

Common mistakes

  • MistakeAssuming a bigger cache always removes conflict misses.

    InsteadConflict misses depend on mapping; arrays aligned to the cache size can thrash even in a large direct-mapped cache. Higher associativity or padding helps.

  • MistakeComputing the index from the full address without removing the offset.

    InsteadDrop the block-offset bits first; the index comes from the bits just above them.

  • MistakeTreating write-through and write-allocate as the same choice.

    InsteadOne decides when hits update the next level, the other whether write misses fill the cache.

  • MistakeJudging a cache by hit time alone.

    InsteadAMAT combines hit time, miss rate and miss penalty; a slightly slower cache with fewer misses often wins.

  • MistakeExpecting larger blocks to help without limit.

    InsteadVery large blocks waste bandwidth, raise the miss penalty and reduce the number of blocks, which increases capacity and conflict misses.

Summary and key terms

  • The memory hierarchy combines small fast memories with large slow ones, relying on locality.
  • Temporal locality favours keeping recent data; spatial locality favours moving data in blocks.
  • A cache address splits into tag, index and offset; associativity sets how many ways each set has.
  • LRU and its approximations choose victims in set-associative caches.
  • Write-through versus write-back and write-allocate versus no-write-allocate are separate choices.
  • Misses are compulsory, capacity, conflict and, in multiprocessors, coherence.
  • AMAT = hit time + miss rate × miss penalty.
Temporal locality
The tendency for recently accessed data to be accessed again soon.
Spatial locality
The tendency for data near recently accessed data to be accessed soon.
Block
The unit of data moved between cache levels; also called a line.
Set-associative cache
A cache divided into sets, where each block may occupy any way of one set.
Tag
The high address bits stored with a block to identify which memory block it holds.
Write-back
A policy that updates the next level only when a modified block is evicted.
Conflict miss
A miss caused by too many active blocks mapping to the same set.
AMAT
Average memory access time: hit time plus miss rate times miss penalty.

Practice problems

Work each problem on paper first, then reveal the answer to check.

  1. A 32 KiB, 8-way cache has 64-byte blocks and 32-bit addresses. Give the offset, index and tag widths.

    Show answer

    Sets = 32768 ÷ (64 × 8) = 64, so offset 6 bits, index 6 bits, tag 20 bits.

  2. A cache has a hit time of 1 cycle, a miss rate of 4 percent and a miss penalty of 60 cycles. What is the AMAT?

    Show answer

    1 + 0.04 × 60 = 3.4 cycles.

  3. Two arrays used together are each 8 KiB and start 8 KiB apart in an 8 KiB direct-mapped cache. What kind of misses dominate, and what are two fixes?

    Show answer

    Conflict misses; increase associativity or pad one array so the two map to different sets.

  4. Why are level 1 instruction and data caches usually separate?

    Show answer

    So that fetch and data access can proceed in the same cycle without a structural hazard, and each cache can be tuned to its access pattern.

Lesson quiz · 5 questions

Score 60% or more to complete this lesson.

Take the quiz