Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 60

Optimizing Cache Performance in Pointer-Intensive Code

Recommended resources

Hello! Welcome back to your course on C pointers.

In our last lesson, we established a crucial principle: modern CPU performance is often limited by memory access, not raw computation. We explored how the CPU cache mitigates this "memory wall" and how access patterns with good spatial and temporal locality lead to high-performance code, while pointer-chasing through scattered data structures like linked lists can be disastrously slow.

Today, we transition from the why to the how. This lesson directly addresses the learning outcome: Apply data layout and access pattern optimizations to improve cache performance in pointer-intensive code. You will learn concrete techniques to structure your data and write loops that work in harmony with the hardware. Given your work in performance-sensitive ADAS communication stacks, these data-oriented design patterns are directly applicable to optimizing message processing, signal handling, and other core tasks.

1. The Philosophy: Data-Oriented Design

Object-oriented programming encourages us to bundle data and the functions that operate on it into objects that model the real world. This is excellent for abstraction and maintainability. However, it can sometimes lead to memory layouts that are inefficient for the hardware.

Data-Oriented Design (DOD) is a complementary approach that prioritizes arranging data in memory for efficient processing, especially in performance-critical loops. The core idea is to think about your program as a series of transformations on streams of data and to organize that data to maximize cache utilization.

To see a great example of this thinking in action, let's turn to a classic talk by Scott Meyers.

code::dive conference 2014 - Scott Meyers: Cpu Caches and Why You Care

In this segment from 'Cpu Caches and Why You Care,' Scott Meyers explains how simply rethinking the location of a single boolean flag can expose a massive performance problem, leading to the idea of data-oriented design.

Please watch the segment from 45:46 to 49:44. Pay attention to how processing a large array of game objects, each with an is_live flag, wastes cache bandwidth and how separating the flags from the objects is the data-oriented solution.

This example of separating the is_live flags from the main object data is the perfect introduction to the most fundamental pattern in data-oriented design: the choice between an Array of Structures and a Structure of Arrays.


2. Array of Structures (AoS) vs. Structure of Arrays (SoA)

This is the central trade-off in data layout for performance. Let's define them with a simple C example. Imagine we're tracking a set of radar points:

Array of Structures (AoS): This is the traditional, object-centric approach. All data for a single entity is grouped together.

struct RadarPoint_AoS {
    float x;
    float y;
    float z;
    float velocity;
    int id;
};

struct RadarPoint_AoS points[1000];

In memory, this looks like: [p0_x, p0_y, p0_z, p0_v, p0_id] [p1_x, p1_y, p1_z, p1_v, p1_id] ...

Structure of Arrays (SoA): This is the data-oriented approach. Each property of the entities is stored in its own contiguous array.

struct RadarPoints_SoA {
    float x[1000];
    float y[1000];
    float z[1000];
    float velocity[1000];
    int id[1000];
};

struct RadarPoints_SoA points;

In memory, this looks like: [p0_x, p1_x, ..., p999_x] [p0_y, p1_y, ..., p999_y] ...

Which one is better? It depends entirely on your access pattern. The following video provides an exceptionally clear explanation of the performance implications.

Code for Game Developers - SoA vs AoS

The video 'Code for Game Developers - SoA vs AoS' by Jorge Rodriguez is a masterclass in explaining this concept. It visually demonstrates how each layout interacts with cache lines and details the profound performance benefits of SoA for certain access patterns.

Please watch from 1:12 to 12:03. The video breaks down: The traditional AoS layout. Why AoS is inefficient for updating positions (pulling unneeded data into the cache). The SoA layout. The three main advantages of SoA: full cache line utilization, enabling hardware prefetching, and simplifying SIMD operations.

To summarize the key points from the video and our definitions:

Aspect Array of Structures (AoS) Structure of Arrays (SoA)
Memory Layout struct { ... } arr[N]; struct { T1 arr1[N]; T2 arr2[N]; ... }
Best For Accessing all or most fields of a single entity at a time. Accessing the same field for many entities at a time.
Cache Impact Can cause cache pollution by loading unused fields into the cache line. Maximizes cache line utility. If you're processing x coordinates, the cache line is filled only with x coordinates.
Example Use "For this one radar point, print all its properties." "For all 1000 radar points, update their positions using their velocities."

This choice is directly relevant to your work. If you're processing a batch of CAN messages to check for a specific signal that appears at the same offset in each message, an SoA-like approach where you just iterate over the relevant bytes would be far more efficient than parsing each message object completely.

Test your understanding!

You are designing a system to process data from a camera's image sensor. You have a large array of Pixel objects. You need to implement a function that adjusts the brightness of the entire image by adding a constant value to the red, green, and blue components of every pixel, but leaves the alpha component untouched.

struct Pixel {
    uint8_t red;
    uint8_t green;
    uint8_t blue;
    uint8_t alpha;
};

Would an AoS or SoA layout be more cache-efficient for this specific adjust_brightness function? Why?

Show answer

An SoA (Structure of Arrays) layout would be more cache-efficient.

The adjust_brightness function needs to access the red, green, and blue fields for all pixels, but it never touches the alpha field.

  • In an AoS layout, the struct Pixel would be laid out as [R, G, B, A, R, G, B, A, ...]. When the CPU processes the RGB values for one pixel, the A value is brought into the cache line alongside them. Since alpha is never used in this operation, 25% of the cache bandwidth and space is wasted.

  • In an SoA layout, you would have separate arrays: uint8_t all_reds[N], uint8_t all_greens[N], uint8_t all_blues[N], and uint8_t all_alphas[N]. The adjust_brightness function would iterate through the reds, greens, and blues arrays. Every byte loaded into the cache from these arrays would be data that the function actually needs. This leads to perfect cache line utilization and allows the hardware prefetcher to work optimally.


3. Advanced Layout and Access Pattern Strategies

While AoS vs. SoA is the foundational choice, other patterns build on the same principles.

Interleaving Data for Algorithmic Locality

Sometimes, an algorithm requires several pieces of data at the same time. The classic example is a Fast Fourier Transform (FFT), which operates on complex numbers. You could store the real and imaginary parts in separate arrays (an SoA approach), but the core "butterfly" calculation needs both the real and imaginary parts for every step.

In this case, an AoS-like layout is better. By interleaving the data, you ensure that when you access data[i].real, the needed data[i].imag is in the same cache line.

Code Optimization Guide for C/C++ Developers (2026)

The 'Code Optimization Guide for C/C++ Developers' from wedolow.com has an excellent section on optimizing an FFT. It demonstrates how changing the data layout is a key part of the optimization.

Please read the section 'Optimized FFT Using Precomputed Twiddle Factors and Interleaved Complex Layout' and pay close attention to subsection 1, 'Contiguous Memory Access That Works With the CPU, Not Against It.' Notice how it explicitly contrasts jumping between separate real and imag arrays with traversing a single, interleaved complex array.

This shows that there is no one-size-fits-all rule. You must analyze the algorithm's access pattern to determine the optimal data layout.

Padding and Alignment for Cache Control

As an embedded engineer, you know the importance of memory alignment. For cache performance, it's also critical.

  • Preventing False Sharing: If two threads on different cores modify variables that happen to be on the same cache line, the cores must constantly invalidate and resynchronize that line, even if the variables are logically independent. You can prevent this by padding your structs to the size of a cache line (typically 64 bytes), ensuring that each object in an array occupies its own unique cache line.
  • Alignment for SIMD: Single Instruction, Multiple Data (SIMD) instructions operate on vectors of data and often require the data to be aligned to a 16, 32, or 64-byte boundary.

Advanced C++ Optimization Techniques for High- ...

The article 'Advanced C++ Optimization Techniques' provides a clear, concise explanation of memory alignment and padding strategies.

Please read the section 'Memory Alignment and Padding.' Focus on the concepts of 'Cache line alignment' and 'Padding for structure size.' The code examples clearly illustrate how to enforce this.

In C, you can use the _Alignas keyword (from C11) or compiler-specific attributes like __attribute__((aligned(64))) to enforce alignment.

Zero-Copy Processing

In communication stacks, a common task is to parse fields from a message buffer. A naive approach might be to malloc memory for each field and strcpy the data. This is terrible for performance: it's slow, churns the heap, and destroys spatial locality by scattering small allocations all over memory.

A "zero-copy" approach avoids this entirely. Instead of copying data, you use pointers to refer to it in-place. A pointer and a length are all you need to represent a "view" of the original data without any allocation.

Code Optimization Guide for C/C++ Developers (2026)

The wedolow.com article also has a superb example of this with a log parser. While it uses C++ string_view, the principle is identical for C pointers.

Please read the section 'How Zero-Copy Parsing Eliminates the Fundamental Bottleneck.' Understand how returning a pointer/length pair (string_view) instead of a new string avoids allocations and keeps all memory access within the original, cached buffer.

In C, we can implement this easily:

// Represents a non-owning "view" of a string.
struct string_view {
    const char* data;
    size_t length;
};

// Zero-copy function to get a field.
struct string_view get_field(const char* buffer, size_t buffer_len) {
    // ... logic to find start and end of field ...
    struct string_view field;
    field.data = &buffer[start_index];
    field.length = end_index - start_index;
    return field; // No allocation, no copy!
}

Conclusion

You now have a powerful set of tools to write pointer-intensive code that is highly optimized for modern CPUs. By consciously designing your data layouts and access patterns, you can prevent the CPU from stalling and achieve significant performance gains.

Key Takeaways:

  • Data-Oriented Design focuses on arranging data for efficient machine processing, not just human abstraction.
  • The AoS vs. SoA trade-off is fundamental. Use SoA when processing the same field of many objects; use AoS when processing many fields of one object.
  • Interleaving related data (e.g., struct Complex) can be an effective AoS-like pattern if an algorithm needs those fields together.
  • Use padding and alignment to control how your structs map to cache lines, preventing false sharing and enabling SIMD.
  • Employ zero-copy techniques using pointers and lengths to parse data from buffers without expensive allocations and copies, maximizing cache locality.

Preview of the Next Lesson

In this lesson, we discussed alignment as a tool for cache optimization. In our next lesson, "Handle pointer alignment requirements for different embedded architectures," we will dive much deeper into this topic. We'll move beyond cache performance and explore how pointer alignment is a strict correctness requirement on many embedded processors (like ARM) and how to handle unaligned data safely and efficiently in your automotive and embedded projects.