A single cache cannot be both fast and large, so processors stack several levels of cache, each one catching what the last one missed. They also try to go one better than waiting for misses: a prefetcher watches the pattern of accesses and fetches data before the program asks for it. This article explains why several levels work, how the levels relate to each other, what inclusion means, how prefetchers work and when they backfire, with results from real address traces.

12 min read | Intermediate

What You'll Learn

  • Explain why a hierarchy of caches beats one large cache
  • Describe how goals differ between L1, L2 and L3
  • Explain inclusive and exclusive cache policies
  • Describe common prefetchers and how to judge them

Why More Than One Level

A first-level cache must answer in a few cycles, because every instruction and most data goes through it. That limits it to tens of kilobytes. A cache large enough to catch most misses would be megabytes, and it would take many cycles to read. The solution is the same as for the whole memory hierarchy: use both.

Levels of a typical hierarchy: L1 caches of about 32 KB at 4 cycles, an L2 cache of 512 KB to a few MB at about 14 cycles, an L3 cache of many MB at about 50 cycles, and DRAM at about 300 cycles

The figures in the picture are rough, for a large core at a high clock. Each step is around three to six times slower and around ten or more times larger. Using AMAT, the average memory access time, with those numbers and assumed miss rates of 5 percent at L1, 40 percent local at L2, and 40 percent local at L3:

AMAT = 4 + 0.05 x (14 + 0.40 x (50 + 0.40 x 300))
     = 4 + 0.05 x (14 + 0.40 x 170)
     = 4 + 0.05 x 82
     = 8.1 cycles

Without L2 and L3, it would be 4 + 0.05 x 300 = 19 cycles. And each level does not need to be perfect, because it only filters what comes through.

Each Level Has a Different Job

L1 caches are built for speed. They are split into instruction and data caches, because the pipeline needs both in the same cycle. They use lower associativity, or careful tricks, to keep the hit time down, and several ports so that a core can do several loads per cycle.

L2 caches are usually private to one core, unified, meaning they hold both instructions and data, and larger. A few extra cycles matter less here, so designers trade them for capacity, higher associativity, and lower power: for example, reading the tags first and only then the one data way that matched.

L3, the last-level cache, is usually shared by all the cores of a chip. Its job is to avoid DRAM, whose latency is so large that almost any hit is worth having. It is also a meeting point, where one core can find data recently used by another. It is often divided into slices across the chip, with the address deciding which slice holds a line, so that access time depends a little on distance.

Line sizes are usually the same at all levels, most often 64 bytes, which keeps transfers between levels simple.

Quick Check

Q: Why are L1 caches split into instruction and data, while L2 is usually unified?

Show Answer

The pipeline fetches an instruction and accesses data in the same cycle, so L1 needs two independent ports, and splitting gives that cheaply. At L2 accesses are far less frequent, so one cache can serve both, and a unified cache shifts its space to whichever a program needs more.

How Levels Relate: Inclusion

When a line is in L1, is it also in L2? There are three common answers:

  • Inclusive: everything in L1 is also in L2. When L2 evicts a line, it must also remove it from L1. The advantage is that L2's tags tell you exactly what might be in L1, which helps with coherence. The cost is that L1's contents are duplicated.
  • Exclusive: a line lives in one level or the other, but not both. A line moves up to L1 on a hit, and when it leaves L1 it goes down to L2. Total capacity is the sum of the two. The cost is more data movement.
  • Non-inclusive: no rule either way. A line may be in both, or just one. This is the simplest and most flexible, but it needs extra tracking to know who holds what.

Designers often use different policies at different levels. Over time, the ratio of L3 size to the size of the private caches has fallen, and pure inclusion has become less attractive, since it wastes more space. Many recent designs use a non-inclusive L3 plus a snoop filter, a separate tag store that tracks what the private caches hold.

Prefetching

Every cache so far waits for a miss. But many miss patterns are predictable. If a program is walking through an array, the next line is an easy guess. A prefetcher makes that guess and brings a line into the cache before it is needed. If it is right, a future miss becomes a hit. It can remove compulsory misses, the kind that no cache size can help with.

Next-Line Prefetch

The simplest prefetcher fetches the next line after a miss:

A row of six cache lines scanned in order. Lines 0, 2 and 4 miss, and each miss also fetches the next line, so lines 1, 3 and 5 are already there

A model of a 1 KB, 2-way data cache with 16-byte lines ran with and without it, on six small programs:

ProgramNo prefetchNext-line prefetch
array sum25.1%12.6%
memory copy25.0%12.5%
checksum3.3%1.8%
matrix multiplication5.7%3.6%
bubble sort0.3%0.1%
linked list36.4%33.4%

The sequential programs halve their misses, which is exactly what the picture predicts. The linked list barely changes, because its next node can be anywhere, and neighbors in memory have nothing to do with each other.

Stride Prefetchers

Many loops step through memory with a fixed gap, such as one field of each element in an array of structures. A stride prefetcher keeps a small table indexed by the address of the load instruction. It records the last address and the difference, or stride. If the same stride repeats, it predicts the next one, and it can run ahead several strides, so the data arrives in time.

Other Prefetchers

Instruction prefetchers fetch the next lines of code, and they work very well on straight code. Software can prefetch as well: RISC-V's Zicbop extension provides prefetch hints, and C compilers offer __builtin_prefetch, so code that walks a known structure can ask for data early. Modern large cores contain several hardware prefetchers at once, including ones that learn repeating sequences of addresses, and these can handle some pointer-chasing patterns.

Judging a Prefetcher

Three measures matter:

  • Coverage: the share of misses it removes.
  • Accuracy: the share of prefetched lines that are actually used.
  • Timeliness: whether the data arrives before it is needed, but not so early that it gets evicted first.

A wrong prefetch has real costs. It uses memory bandwidth, and it pushes a possibly useful line out of the cache. A very aggressive prefetcher can make a program slower, especially on a shared cache or a loaded memory system, where it takes capacity from other cores. Good designs watch their own accuracy, and back off when it falls. Prefetched lines are often placed into a small side buffer or at low priority, so that a wrong guess costs less.

Where Is This Used?

Every modern application processor has hardware prefetchers at several levels, and they account for a large part of its real-world speed. Many have model-specific controls, and in servers these are sometimes turned off for one workload that they hurt. In microcontrollers, a much simpler form is common: a small instruction prefetch buffer in front of slow flash memory, which reads the next line of code ahead of time, so that straight-line code does not suffer flash wait states.

Small Tricks Between the Levels

Several small structures sit between the levels and remove a surprising share of misses. A victim cache, proposed in 1990, is a tiny fully associative buffer that holds lines just evicted from L1. If a line is needed again soon after being pushed out, it can be swapped back in a cycle or two instead of being fetched from L2. It is a cheap cure for conflict misses in a low-associativity cache.

Way prediction keeps a small record of which way of a set was used last, and reads only that way, saving the power of reading them all. A correct guess is as fast as a direct-mapped cache, and a wrong guess costs a cycle. Critical word first delivers the wanted word of a line before the rest. And a load buffer holds a line that has just arrived, so that the next few accesses can be served at once while the cache array is busy with something else.

None of these changes the big picture. Each one takes a small amount of area to remove a part of the cost at a boundary between levels. Good cache designs are full of such details, and they account for much of the difference between two caches with the same headline size.

Some Costs of Many Levels

More levels bring side effects. A miss that goes all the way to DRAM must first fail at each level, and the tag checks add to its latency. So some designs start a DRAM access in parallel with the L3 lookup, or they predict that a miss is coming and go straight to memory. Static power rises with the amount of SRAM, and in many chips caches take more than half the die, so leakage is significant. And the whole structure needs to stay coherent, as Multicore Processors and Shared Memory describes.

Whether all of this is needed depends on the product. A small microcontroller has no cache, or one small one. A phone core has three levels, and a server chip may add yet another very large cache stacked on top of the die.

Quick Check

Q: A prefetcher fetches 1,000 lines, of which 700 are used, and removes 700 of the 2,000 misses a program had. What are its coverage and accuracy? What happened to the other 300?

Show Answer

Coverage: 700 / 2,000 = 35 percent. Accuracy: 700 / 1,000 = 70 percent. The other 300 lines used memory bandwidth for nothing, and each may have evicted a line that would have been useful.

Common Mistakes

Mistake #1: Prefetching too late

A prefetch that arrives only just before the demand access saves little. Prefetchers have to run far enough ahead to allow for the miss penalty, and that distance must grow as memory gets slower.

Mistake #2: Forgetting bandwidth

In a shared system, one core's aggressive prefetching uses memory bandwidth that other cores need. Evaluate prefetchers with a loaded memory system and several programs running, rather than one program alone.

Mistake #3: Sizing each level alone

Each level's value depends on the others. A bigger L1 reduces the traffic to L2, which changes what the L2 should be. Evaluate the whole hierarchy with AMAT or a full simulation.

Practice Exercise

Challenge: Is the Prefetcher Worth It?

A cache has a 1-cycle hit time, a 10 percent miss rate and a 40-cycle penalty. These numbers are assumptions for this exercise. A prefetcher has 50 percent coverage and is always on time. Wrong prefetches add load on memory, raising the penalty to 44 cycles. (a) What is the AMAT without the prefetcher? (b) With it? (c) What is the speedup in memory time? (d) If coverage is only 5 percent on a pointer-chasing program, and the penalty still rises to 44 cycles, what is the AMAT then?

Hint

Coverage reduces the miss rate. The new miss rate is 10 percent x (1 - coverage).

Solution

(a) 1 + 0.10 x 40 = 5 cycles. (b) 1 + 0.05 x 44 = 3.2 cycles. (c) 5 / 3.2 = 1.56. (d) 1 + 0.095 x 44 = 5.18 cycles, slower than having no prefetcher at all. That is why real prefetchers turn themselves down when they are not helping.

Summary

  • Several levels give both a fast hit and a low overall miss rate.
  • L1 is built for speed, L2 for capacity per core, and L3 to avoid DRAM and share data between cores.
  • Inclusive, exclusive and non-inclusive policies trade duplication against tracking.
  • A prefetcher fetches lines before they are needed. A next-line prefetcher halved misses on sequential programs in one test.
  • Judge a prefetcher by coverage, accuracy and timeliness, because wrong prefetches cost bandwidth and space.
Did you find what you were looking for?

Quiz

No quiz questions available for this article.