Introduction
Welcome to the final lesson in our module on memory hierarchy and cache systems. In the previous lesson, we quantified the impact of cache performance by calculating metrics like hit rate, miss penalty, and Average Memory Access Time (AMAT). You saw firsthand how even a small miss rate can drastically increase the time it takes to access memory, slowing down the entire program.
Today, we will connect that quantitative understanding to the real world of programming. Our goal is to learn how to identify cache-unfriendly memory access patterns directly in C++ code. We will move beyond abstract metrics and start developing an intuition for spotting code that is likely to cause performance-killing cache misses. This skill forms the foundation for the optimization techniques we will explore in the next module.
By the end of this lesson, you will be able to analyze a C++ code snippet and identify common patterns that lead to poor cache performance, such as non-sequential memory access, pointer chasing, inefficient data layouts, and false sharing.
The Principle of Data Locality
Before we dive into specific code patterns, let's establish the fundamental principle that governs cache efficiency: data locality. The core reason for poor cache performance is failing to use the data that the CPU has already fetched.
As you know, when the CPU needs a piece of data from RAM, it doesn't just fetch that one byte. It pulls in an entire cache line—a contiguous block of memory, typically 64 or 128 bytes. A cache hit occurs when the next piece of data the CPU needs is already in a cache line it holds. A miss occurs when it isn't.
Data Locality · Optimization Patterns
To build a strong mental model for this, let's start with an excellent article that uses an analogy to explain why data locality is so crucial for performance. It sets the stage perfectly for the rest of our discussion.
Please read the "Intent", "Motivation", and the first part of "The Pattern" sections. Focus on the accountant-and-warehouse analogy to understand the relationship between the CPU, RAM, and the cache. The key takeaway is simple: organize your data so that the things you process are next to each other in memory.
The key insight is that performance is not just an attribute of your code's logic (the algorithm), but also of your data's layout in memory. The goal is to arrange data to maximize the use of each cache line brought in from RAM.
To visualize how this works with a familiar data structure, consider a simple 2D array in C++.

With this foundation, let's examine specific C++ patterns that violate the principle of data locality.
Pattern 1: Strided Memory Access
The most classic example of a cache-unfriendly pattern is accessing memory with a large, regular "stride" instead of sequentially. The canonical example is traversing a 2D matrix.
In C++, 2D arrays are stored in row-major order. This means that elements of the same row are contiguous in memory.
- Traversing row by row (
for (row) { for (col) }) results in stride-1 access. This is highly cache-friendly because you are walking through memory sequentially. The CPU's prefetcher can predict this pattern and load the next cache line before you even need it. - Traversing column by column (
for (col) { for (row) }) results in stride-N access, where N is the number of elements in a row. You access one element, then jump N elements forward in memory to get the next one. This is extremely cache-unfriendly.
code::dive conference 2014 - Scott Meyers: Cpu Caches and Why You Care
The following video segment by Scott Meyers provides a superb demonstration of this exact problem. It shows benchmark results comparing row-major and column-major traversal and explains why the performance difference is so dramatic.
Watch from 01:10 to 03:54, which covers the matrix traversal example. Then, watch from 21:17 to 24:10, where he explains why the row-major traversal is faster by discussing cache lines and prefetching.

Identifying the Pattern: Look for nested loops iterating over multi-dimensional arrays. If the innermost loop iterates over an index that does not correspond to the contiguous dimension in memory, you have found a strided access pattern.
Pattern 2: Pointer Chasing
Your experience with data structures like linked lists, trees, and graphs is valuable here. These structures are often implemented using pointers, where each node is a separate heap allocation. While flexible, this is often disastrous for cache performance.
When you traverse a linked list, you follow a pointer from one node to the next. Since new Node() can return memory from anywhere in the heap, the nodes are likely scattered all over memory.
- Accessing
node->valueis a cache hit (assuming you just got the node). - Accessing
node->next->valueis almost guaranteed to be a cache miss, becausenode->nextpoints to a completely different memory location.
This is called pointer chasing.
Cache Friendly C++ - Jonathan Müller - Meeting C++ 2025
The following video by Jonathan Müller explains why containers with contiguous storage are inherently more cache-friendly than those based on nodes and pointers.
Please watch from 25:40 to 30:00. This segment contrasts the memory layouts of std::vector, std::list, std::set, and std::unordered_map, explaining why the pointer-based structures are not cache-friendly.
A common scenario in C++ is having a std::vector<std::unique_ptr<MyObject>>. While the vector of pointers itself is contiguous, the MyObject instances they point to are scattered across the heap. Iterating through this vector and dereferencing each pointer to do work will cause a cache miss for almost every object.
Contiguous arrays in "Data Locality"
The "Data Locality" article we looked at earlier has a fantastic, practical example of this. It shows how a typical Component-based design in game development can lead to pointer chasing and how to refactor it for better performance.
Read the section "Contiguous arrays". Pay close attention to the "before" code with its array of pointers to GameEntity, and the "after" code, which uses flat arrays of components. This is a powerful demonstration of moving from a cache-unfriendly to a cache-friendly design.
Identifying the Pattern: Be suspicious of code that iterates through a collection of pointers and dereferences them to access data. This applies to std::list, std::map, and collections of smart pointers like std::unique_ptr or std::shared_ptr. The more cache-friendly alternative is often a flat std::vector of objects.
Pattern 3: Inefficient Data Layout (Hot/Cold Splitting & AoS vs. SoA)
Even if your data is in a contiguous array, you can still have cache-unfriendly patterns. Consider an array of structs or classes. If each object contains a mix of data—some fields accessed frequently ("hot") and others rarely ("cold")—you are wasting precious cache space.
Every time you access a hot field, the entire object (or at least the part that fits in a cache line) is loaded. The cold data comes along for the ride, occupying cache space but never being used.
Hot/cold splitting in "Data Locality"
This pattern is best understood through an example. The "Data Locality" article clearly illustrates this with a technique called "hot/cold splitting".
Please read the section "Hot/cold splitting". It describes a scenario with an AIComponent that has both frequently used fields (hot) and rarely used fields (cold), and shows how to separate them to improve cache utilization.
This idea can be taken further into a design paradigm known as Data-Oriented Design, which often contrasts two layouts:
- Array of Structures (AoS): The typical object-oriented approach.
std::vector<Person>, wherestruct Person { std::string name; int age; };. This is cache-unfriendly if your loop only needs theage. - Structure of Arrays (SoA): A data-oriented approach.
struct People { std::vector<std::string> names; std::vector<int> ages; };. If your loop only needs ages, it can now iterate through a tightly packed, contiguous array of integers, which is extremely cache-friendly.
Cache Friendly C++ - Jonathan Müller - Meeting C++ 2025
Jonathan Müller's talk on Cache-Friendly C++ provides an excellent explanation of this AoS vs. SoA trade-off.
Watch the segment from 32:56 to 39:39. It introduces data-oriented design and contrasts the OOP (AoS) approach with the DOD (SoA) approach using clear examples for Person and Shape objects.
Identifying the Pattern: Look for loops that process an array of large objects but only touch one or two small fields within each object. This is a sign that your data layout is inefficient. The hot and cold data are mixed, polluting the cache.
Pattern 4: False Sharing
This last pattern is specific to multithreaded programming, but it is so important and counter-intuitive that we must cover it now. It will be critical for the parallel programming modules to come.
False sharing occurs when:
- Multiple threads are running on different CPU cores.
- They access and modify logically independent variables.
- These variables happen to reside on the same cache line.
When Thread 1 on Core 1 modifies its variable, the cache coherency protocol invalidates the entire cache line for all other cores. When Thread 2 on Core 2 then tries to access its own independent variable, it triggers a cache miss because its copy of the cache line was invalidated. The line has to be fetched again. This back-and-forth invalidation can destroy performance, even though the threads are not sharing any data in the program's logic.
code::dive conference 2014 - Scott Meyers: Cpu Caches and Why You Care
This is a tricky concept, and a good visual explanation is invaluable. Scott Meyers provides one of the clearest examples available, complete with dramatic benchmark results.
First, watch from 04:09 to 10:51. This section introduces a parallel algorithm and shows its terrible scaling due to false sharing, then shows the fix. Later, watch from 38:04 to 40:07, where he formally lists the conditions required for false sharing to occur. The performance graph he shows is a powerful illustration of the problem.
Identifying the Pattern: In a multi-threaded C++ program, look for an array where different threads write to different indices (e.g., results[thread_id] = ...). If the elements of the array are small (like int or double), multiple elements will fit on a single 64-byte cache line. If thread_id 0 and 1 are working on adjacent elements, they are very likely to be causing false sharing.
Conclusion
Today we demystified the connection between your C++ code and the hardware's cache performance. You are now equipped to look at code not just for its logical correctness, but also for its mechanical sympathy with the underlying hardware.
Key Takeaways:
- Data Locality is King: The primary goal is to arrange data contiguously in the order it will be processed to maximize the use of every cache line.
- Common Unfriendly Patterns:
- Strided Access: Accessing multi-dimensional arrays in a non-contiguous order (e.g., column-wise in C++).
- Pointer Chasing: Traversing data structures like linked lists or trees of heap-allocated objects.
- Inefficient Layouts: Mixing frequently accessed ("hot") and rarely accessed ("cold") data in the same object, leading to wasted cache space.
- False Sharing: When independent variables used by different threads happen to fall on the same cache line, causing constant invalidations.
Understanding these patterns is the first and most crucial step toward writing high-performance code.
Preview of the Next Lesson
This lesson concludes our module on the fundamentals of computer organization and memory. You now understand how the CPU executes instructions and how the memory hierarchy impacts performance.
In our next module, "Single-Core Performance Optimization," we will put this knowledge into action. The first lesson will introduce you to profiling C++ code to identify performance bottlenecks. You will learn how to use tools that can pinpoint exactly where your program is spending its time—and which functions are causing the most cache misses—so you can apply the optimization techniques you will learn next.