Hello! Let's dive into our next topic.
In our previous lesson, we implemented an intrusive linked list, a common pattern in systems programming. We briefly mentioned that one of its advantages over non-intrusive lists is potentially better cache performance. Today, we're going to explore exactly what that means and why it's one of the most critical factors in modern software performance.
This lesson focuses on the learning outcome: Explain how cache locality (spatial and temporal) impacts the performance of pointer-based data access. Understanding the interaction between your code's memory access patterns and the underlying hardware is essential for writing truly high-performance applications, a skill highly valued in your work with ADAS and communication stacks.
1. The "Memory Wall": Why Caches Exist
Modern CPUs are incredibly fast, capable of executing billions of instructions per second. However, main memory (RAM) has not kept pace. Accessing data from RAM can take hundreds of CPU cycles, during which the CPU is effectively stalled, waiting for data. This is often called the "memory wall."
To bridge this speed gap, CPUs use a memory hierarchy. At the top are the CPU registers (fastest), followed by several levels of cache memory (L1, L2, L3), and finally the large but slow main memory (RAM).
A cache is a small, extremely fast, and expensive piece of memory located on or very close to the CPU core. When the CPU needs data from a memory address, it first checks the cache.
- If the data is in the cache (a cache hit), it's retrieved almost instantly.
- If the data is not in the cache (a cache miss), the CPU must fetch it from the much slower main memory, causing a significant delay.
To get a more intuitive grasp of this system, please watch the following video. It uses a great real-world analogy to explain the memory hierarchy and introduces the concepts of prefetching and cache lines.
Memory, Cache Locality, and why Arrays are Fast (Data Structures and Optimization)
This video, 'Memory, Cache Locality, and why Arrays are Fast' from SimonDev, provides an excellent introduction to CPU caches, hits, misses, and the hardware prefetcher.
Please watch the following segments: 3:02 - 4:10: Focus on the library assistant analogy to understand the roles of the cache and prefetcher. 4:31 - 6:01: This part details the L1, L2, and L3 cache levels and explains the mechanism of a cache miss, introducing the crucial concept of a cache line (typically 64 bytes). 6:01 - 6:48: Learn about the hardware prefetcher, which tries to predict which memory you'll need next.
The key takeaway is that memory is not moved byte by byte. When a cache miss occurs, the CPU fetches a whole cache line—a contiguous block of memory (e.g., 64 bytes)—from RAM. This single architectural detail is the foundation of cache performance optimization.
2. The Principles of Locality
The effectiveness of caches hinges on a principle known as locality of reference, which describes the predictable way that typical programs access memory. It has two main forms: temporal and spatial locality.
Make your programs run faster by better using the data cache
The article 'Make your programs run faster by better using the data cache' by Johnny's Software Lab clearly defines these two principles.
Please read the short section titled 'Why does the memory cache make the system run faster?'. It provides concise definitions of temporal and spatial locality with simple examples.
Let's formalize those definitions:
Spatial Locality
This principle states that if a program accesses a particular memory location, it is highly likely to access memory locations nearby in the immediate future.
- How it relates to cache: When the CPU fetches a 64-byte cache line for a single 4-byte integer, it also gets the next 15 integers for free. If your code accesses them sequentially, those subsequent accesses will be lightning-fast cache hits.
- Pointer Example: Traversing an array using pointer arithmetic (
ptr++) is the classic example of leveraging spatial locality. The memory addresses are contiguous, which is perfect for the cache and the hardware prefetcher.
Temporal Locality
This principle states that if a program accesses a particular memory location, it is highly likely to access that same memory location again in the near future.
- How it relates to cache: Once a memory location is loaded into the cache, subsequent accesses to it will be cache hits until that cache line is evicted (replaced by other data).
- Pointer Example: Repeatedly dereferencing a pointer to access a status field in a control structure within a loop. The pointer's value and the data it points to will likely remain in the cache for the duration of the loop.
3. Pointer Access Patterns and Their Cache Impact
How you use pointers to access data determines whether you are working with or against the cache.
Good Locality: The Array
An array is a contiguous block of memory. Traversing it is the gold standard for cache efficiency. Whether you use array indexing (arr[i]) or a pointer (*(p+i)), you are accessing adjacent memory locations. This predictable, linear access pattern is what the hardware prefetcher is designed to accelerate.
Watch this video to see a stunning demonstration of the performance difference between a cache-friendly access pattern and a cache-unfriendly one.
C++ cache locality and branch predictability
In 'C++ cache locality and branch predictability' by mCoding, the host benchmarks summing vector elements in sequential versus random order. The results clearly show the penalty of poor cache locality.
Please watch from 2:42 to 4:32. Pay close attention to the graph that shows performance degrading as the data size exceeds the cache size for the random access pattern, while the sequential pattern remains unaffected.
Poor Locality: The Linked List and Pointer-Chasing
Now, consider a linked list. Each node is typically allocated independently on the heap, meaning the nodes can be scattered randomly across memory. To get from one node to the next, you must follow a pointer—an operation known as pointer-chasing.

This access pattern exhibits terrible spatial locality:
- You access
node_A. This causes a cache miss, and a 64-byte chunk of memory aroundnode_Ais loaded into the cache. - You then follow
node_A->nexttonode_B, which might be megabytes away in memory. This is another guaranteed cache miss. - The cache line loaded for
node_Ais now useless, as none of the nearby data was needed. You've polluted your cache with irrelevant data.
This constant jumping around memory defeats the hardware prefetcher and leads to a cascade of expensive cache misses.
The following benchmark provides hard numbers on just how significant this difference is.
Beating the Cache: How to Make Your C++ Code Orders of Magnitude Faster
The article 'Beating the Cache' provides a fantastic benchmark comparing linear, strided, and pointer-chasing access patterns.
Please read section 2, 'Comparing Linear vs Non-Linear Memory Access.' Focus on the benchmark setup and the final results table. Notice the dramatic difference in execution time between 'Linear access' and 'Pointer chasing'.
The result is clear: pointer-chasing through a scattered data structure is orders of magnitude slower than linear access through a contiguous one. This is why, for pure traversal performance, arrays and vectors are almost always preferred over linked lists on modern CPUs.
Test your understanding!
Consider these two functions. Both process 1,000,000 Message objects.
// Scenario 1: Array of structures
struct Message { int id; char payload[60]; };
struct Message message_array[1000000];
void process_messages_array() {
for (int i = 0; i < 1000000; ++i) {
process(message_array[i].id);
}
}
// Scenario 2: Array of pointers to structures
struct Message* message_pointers[1000000];
// Assume this array is filled with pointers to Message objects
// allocated individually on the heap.
void process_messages_pointers() {
for (int i = 0; i < 1000000; ++i) {
process(message_pointers[i]->id);
}
}
Which function do you expect to be faster, and why? Explain in terms of spatial locality and cache hits/misses.
Show answer
process_messages_array() will be significantly faster.
The message_array stores the Message objects contiguously in memory. Each Message is 64 bytes (4 + 60). When the loop accesses message_array[0], the CPU loads a 64-byte cache line, which contains the entire first Message object. The very next access, message_array[1], is to the adjacent memory block, which the hardware prefetcher has likely already started loading into the cache. This results in excellent spatial locality and a very high cache hit rate.
In contrast, process_messages_pointers() iterates over an array of pointers. While the pointers themselves are contiguous, the Message objects they point to were allocated individually and are scattered across the heap. Each access message_pointers[i]->id involves a pointer-chase. This will likely result in a cache miss for every single iteration, as the CPU has to fetch a new, unpredictable memory location from RAM, leading to very poor performance.
Conclusion
You now have a solid foundation for understanding one of the most important aspects of software performance on modern hardware. Writing cache-aware code is not premature optimization; it's a fundamental aspect of efficient systems design.
Key Takeaways:
- CPUs use caches to hide the latency of slow main memory. Performance is dominated by the ratio of cache hits to cache misses.
- Memory is managed in cache lines (typically 64-byte blocks).
- Spatial Locality (accessing nearby data) is king. Data structures that lay out memory contiguously, like arrays, are highly efficient.
- Temporal Locality (re-accessing the same data) is also beneficial, as it keeps frequently used data in the fast cache.
- Pointer-chasing, common in data structures like linked lists or trees, destroys spatial locality and leads to frequent, expensive cache misses.
Preview of the Next Lesson
In this lesson, we explored the why of cache performance. In the next lesson, we will focus on the how. We'll dive into practical techniques for applying data layout and access pattern optimizations to improve cache performance in pointer-intensive code. We'll cover topics like Structure of Arrays (SoA) vs. Array of Structures (AoS) and data alignment, turning these theoretical principles into concrete performance gains.