Introduction
Hello! In our last lesson, we explored the spectrum of cache designs, comparing direct-mapped, set-associative, and fully-associative caches. We learned how increasing associativity reduces conflict misses at the cost of hardware complexity and hit time. You now have a solid grasp of the structure of CPU caches.
Today, we'll connect this hardware knowledge to the software you write. Our goal is to analyze cache hits and misses by applying the principles of spatial and temporal locality. We will investigate why some memory access patterns are inherently faster than others and learn to identify these patterns in C++ code. Understanding this is the first step toward writing high-performance software that works with the hardware, not against it.
The Principle of Locality
Caches work because programs don't access memory randomly. Instead, they exhibit a property called the principle of locality. This principle has two main facets.
To start, let's get a clear and concise definition of these concepts.
This short video introduces the two types of locality and explains how caches are designed to take advantage of them.
Please watch from the beginning until 01:58 to understand the core definitions of temporal and spatial locality.
Let's formalize those definitions:
-
Temporal Locality (Locality in Time): The tendency to reuse a specific piece of data within a short time frame. If a memory location is accessed, it's highly likely to be accessed again soon.
- How caches exploit this: By storing recently accessed data, caches ensure that subsequent accesses become fast hits instead of slow misses. A loop variable or a running sum are great examples.
-
Spatial Locality (Locality in Space): The tendency to access data at memory locations near other recently accessed locations. If a memory location is accessed, it's highly likely that nearby addresses will be accessed soon.
- How caches exploit this: Memory is not fetched byte-by-byte. Instead, it's loaded into the cache in fixed-size blocks called cache lines (typically 64 bytes). When you have a miss and request one byte, the hardware gives you the entire 64-byte line it belongs to, pre-loading adjacent data for free.


Locality in Action: The Matrix Traversal Example
Understanding locality is one thing, but seeing its impact is what makes the concept stick. Let's look at a classic example that reveals the dramatic performance implications of writing cache-friendly code.
Consider traversing a 2D matrix stored in row-major order, which is the default for C++.
// Row-major layout: elements of a row are contiguous in memory
int matrix[ROWS][COLS];
// Traversal 1: Row-major access
for (int i = 0; i < ROWS; ++i) {
for (int j = 0; j < COLS; ++j) {
sum += matrix[i][j]; // Accessing matrix[i][0], matrix[i][1], ...
}
}
// Traversal 2: Column-major access
for (int j = 0; j < COLS; ++j) {
for (int i = 0; i < ROWS; ++i) {
sum += matrix[i][j]; // Accessing matrix[0][j], matrix[1][j], ...
}
}
Both loops perform the exact same number of additions and access the same memory locations, just in a different order. Intuitively, their performance should be similar. The reality is starkly different.
code::dive conference 2014 - Scott Meyers: Cpu Caches and Why You Care
The following clip from Scott Meyers' legendary talk 'Cpu Caches and Why You Care' demonstrates this performance difference and then explains precisely why it happens. This is a foundational insight for any performance-conscious developer.
Please watch these two segments: 00:43 - 04:09: This sets up the problem, showing benchmark results where row-major traversal is significantly faster and more scalable than column-major. 21:17 - 24:16: This explains the 'why'. It connects the concept of cache lines directly to the memory access patterns of the two traversals.
As the video makes clear:
- Row-major traversal exhibits excellent spatial locality. When
matrix[i][0]is accessed, a cache miss may occur, but the hardware loads a cache line containingmatrix[i][0],matrix[i][1],matrix[i][2], etc. The subsequent loop iterations find the data already in the cache, resulting in fast hits. This is a linear, predictable access pattern that hardware loves. - Column-major traversal exhibits poor spatial locality. Accessing
matrix[0][j]and thenmatrix[1][j]involves jumping across memory by an entire row's worth of bytes (the stride). If the stride is larger than a cache line, every access could result in a new cache miss, fetching a 64-byte line only to use a single integer from it.
This difference between a stride-1 access (row-major) and a large-stride access (column-major) is one of the most important patterns to recognize when analyzing code for cache performance.
A Framework for Analysis: Types of Cache Misses
When analyzing performance, it's useful to categorize why a miss occurred. This helps you reason about potential solutions.
Cache Memories, Cache Complexity
This document provides a concise summary of the common types of cache misses. Understanding these categories gives you a vocabulary for diagnosing performance issues.
Please read the slide titled 'Cache issues' (page 9). It defines five types of misses. Focus on the first three, known as the '3 Cs'.
Let's summarize the key categories:
- Compulsory (or Cold) Miss: The very first access to a block of data. These are unavoidable; the data has to be loaded into the cache at least once. Prefetching can help hide the latency, but the miss itself will happen.
- Capacity Miss: The cache cannot contain all the data needed by the program. A block was loaded, then evicted because the cache was full, and is now needed again. This happens when the program's "working set" (the data it's actively using) is larger than the cache.
- Conflict Miss: The miss occurs because too many active data blocks map to the same cache set (or line in a direct-mapped cache), causing them to evict each other. This can happen even when the cache is not full. This is the type of miss that increasing associativity, as we discussed in the last lesson, is designed to reduce.
The other two misses mentioned, True and False sharing, are related to multi-core systems and will become critical when we discuss parallel programming in a later module.
Hands-On Analysis
Now it's time to apply these concepts. Let's analyze a few code snippets. For each one, consider the memory access pattern and try to predict:
- What type(s) of locality does it exhibit?
- What kind of cache misses are likely to dominate?
We'll use the exercises from the Cache Memories, Cache Complexity PDF you just looked at. Assume sizeof(int) is 4 bytes and a cache line is 64 bytes. This means one cache line can hold 64 / 4 = 16 integers.
Exercise 1: Sequential Access
#define S (1 << 20) // 1,048,576
int A[S];
for (int i = 0; i < S; i++) {
read A[i];
}
- Your Analysis: How does this loop access memory? What kind of locality does it have? What will the hit/miss pattern look like?
Click to see the solution
- Locality: This code has excellent spatial locality. It's a perfect stride-1 access pattern, moving sequentially through the array
A. - Misses: The first access to each 16-element chunk will be a compulsory (cold) miss. The next 15 accesses in that chunk will be hits, because the entire cache line was loaded on the first miss.
- Pattern: Miss, Hit, Hit, ..., Hit (15 times), Miss, Hit, ...
- Hit Rate: Approximately 15/16 or 93.75%.
Exercise 2: Repeated Access
#define S (1 << 20)
int A[S];
for (int i = 0; i < S; i++) {
read A[0];
}
- Your Analysis: What's the access pattern here? What locality does it show?
Click to see the solution
- Locality: This code has excellent temporal locality. It accesses the exact same memory location,
A[0], over and over. - Misses: The very first read of
A[0]will be a compulsory miss. After that,A[0](and its cache line) will be in the cache. - Pattern: Miss, Hit, Hit, Hit, ...
- Hit Rate: Approaches 100% after the first access.
Exercise 3: Strided Access
This one is trickier. What if the stride is not 1?
#define S (1 << 20)
#define N 14 // Assume 14 <= N
int A[1 << N];
for (int i = 0; i < S; i++) {
// Note the stride of 16 integers (64 bytes)
read A[(i * 16) % (1 << N)];
}
- Your Analysis: The loop accesses elements
A[0],A[16],A[32], etc. Each step jumps 16 integers, which is exactly 64 bytes. What does this mean for spatial locality?
Click to see the solution
- Locality: This code has no spatial locality. Each access jumps exactly one cache line. When
A[0]is read, its 64-byte line is fetched. The next access is toA[16], which is the first element of the very next cache line. The 15 other integers that were fetched along withA[0]are never used. - Misses: Every single access will land in a new cache line. If the portion of the array being accessed (
1 << Nelements) is larger than the cache size, it's very likely that each access will be a miss (either compulsory or capacity/conflict). - Pattern: Miss, Miss, Miss, ...
- Hit Rate: Close to 0%. This is a cache-unfriendly pattern.
Conclusion
In this lesson, we bridged the gap between cache hardware and program behavior. You now have the conceptual tools to look at a piece of code and reason about its performance in the memory hierarchy.
Key Takeaways:
- Temporal Locality: Reusing the same data soon. Caches exploit this by keeping recent data around.
- Spatial Locality: Accessing nearby data soon. Caches exploit this by fetching data in blocks called cache lines.
- Stride matters: A stride of 1 (sequential access) is ideal for spatial locality. Large strides that jump across cache lines are inefficient.
- The performance difference between a cache-friendly (row-major) and cache-unfriendly (column-major) matrix traversal can be an order of magnitude or more.
- Cache misses can be categorized as Compulsory, Capacity, or Conflict, which helps in diagnosing performance issues.
Preview of the Next Lesson
We have learned to qualitatively analyze code for cache hits and misses. In our next lesson, we will move to quantitative analysis. We will learn how to calculate cache performance metrics: hit rate, miss penalty, and average memory access time (AMAT). This will allow us to put concrete numbers on the performance impact of the access patterns we analyzed today.