Skip to main content

Welcome to a Grasp lesson

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

Lesson 11

Memory Alignment and Padding in Structs

Recommended resources

Hello! Welcome to your next lesson on the path to building a high-performance telemetry engine.

In our previous lesson, we mastered RAII and smart pointers, which automate the lifetime management of memory. This is crucial for writing safe and correct C++ code. However, for a high-performance systems project like yours, managing how that memory is laid out is just as important as managing when it's freed.

This brings us to today's topic, which is fundamental to Data-Oriented Design and achieving the "zero-overhead" goal of your project. Our learning outcome is to describe memory alignment requirements and padding in structs. We will uncover why the sizeof a struct is often greater than the sum of its parts and how you can use this knowledge to optimize your data structures for maximum performance.

Let's dive into the "why" before we get to the "how."

Why Alignment Matters: A Look at the Hardware

At a high level, a CPU doesn't read memory one byte at a time. To be efficient, it fetches data from RAM in larger, fixed-size chunks called cache lines. On most modern x86-64 architectures, a cache line is 64 bytes. When the CPU needs data from a certain memory address, it loads the entire 64-byte block containing that address into its high-speed cache (L1/L2/L3).

This has a profound implication: if a 4-byte integer or an 8-byte double is split across the boundary of two cache lines, the CPU must perform two memory fetches instead of one to retrieve it. This is called an unaligned memory access.

The following video provides an excellent deep dive into the memory hierarchy and explains exactly why we want our data to fit neatly within these cache lines.

Why structs are larger than they appear and how to fix it | Memory alignment Explained

The video 'Why structs are larger than they appear' by codemastercpp explains the connection between CPU caches, cache lines, and the need for memory alignment.

Please watch from 1:14 to 13:30. This is the core theoretical foundation for today's lesson. Focus on understanding: The memory hierarchy (registers, cache, RAM). What a cache line is (a 64-byte block of memory transfer). The performance penalty of an access that crosses a cache line boundary. The formal definition of alignment: a data type of size N is aligned when its memory address is a multiple of N.

As you saw, aligned access is fast because it requires a single cache line load. Unaligned access is slow because it can require two loads plus extra work inside the CPU to stitch the data back together.

Unaligned Memory Access Process
This diagram visualizes the work a CPU must do for an unaligned read. It has to fetch two separate chunks from memory, shift them, and combine them to reconstruct the desired data, introducing significant overhead.

On some architectures, like certain ARM or MIPS processors, an unaligned access isn't just slow—it causes a hardware exception and crashes the program. While modern x86 CPUs handle it, the performance penalty is real and something we must avoid in high-performance code.


Structure Padding: The Compiler's Automatic Fix

So, how does C++ ensure that data members within a struct are aligned? The compiler does it for us by inserting unused bytes called padding.

Let's consider this struct:

struct Example {
    char a;  // 1 byte
    int  b;  // 4 bytes
    char c;  // 1 byte
};

You might expect the size of this struct to be 1 + 4 + 1 = 6 bytes. However, the compiler has to follow two fundamental rules:

  1. Member Alignment: Each member must start at a memory address that is a multiple of its alignment requirement. A char can be anywhere (alignment of 1), but an int typically needs to be at an address divisible by 4.
  2. Struct Alignment: The total size of the struct itself must be a multiple of the alignment of its strictest (largest) member. In this case, the int has an alignment of 4, so the total size of Example must be a multiple of 4.

Let's see how the compiler applies these rules and adds padding.

C++ Weekly - Ep 410 - What Are Padding and Alignment? (And Why You Might Care)

This short video, 'What Are Padding and Alignment?' from C++ Weekly, provides a practical demonstration using sizeof and alignof to reveal the padding added by the compiler.

Watch from 1:18 to 6:19. Jason Turner will show you how to inspect alignment and size, and then walk through the memory layout of a padded struct, demonstrating how padding works in practice.

Let's break down the memory layout of our Example struct, assuming a 4-byte alignment for int:

  • char a; is placed at offset 0. Size: 1 byte.
  • The next available address is offset 1.
  • int b; requires 4-byte alignment, so it cannot start at offset 1. The next valid address is offset 4.
  • The compiler inserts 3 bytes of padding after a.
  • int b; is placed at offset 4. Size: 4 bytes.
  • The next available address is offset 8.
  • char c; is placed at offset 8. Size: 1 byte.
  • The total size is now 1 (a) + 3 (pad) + 4 (b) + 1 (c) = 9 bytes.
  • Now for Rule #2: The strictest alignment is 4 (from int). The total size must be a multiple of 4. The next multiple of 4 after 9 is 12.
  • The compiler adds 3 bytes of tail padding at the end.
  • Final sizeof(Example) is 12 bytes, not 6. This final padding ensures that in an array Example arr[10];, every arr[i].b will be correctly aligned.
Memory Alignment and Padding in C++ Structs
This image shows a 'before' and 'after' of a struct's memory layout. On the left is a hypothetical packed layout. On the right is the actual layout after the compiler adds padding to align the `long x` member, increasing the total size but ensuring efficient access.

Optimizing Struct Layout

This automatic padding wastes memory. In our Example struct, 50% of its size is unused padding! For your telemetry engine, which will process millions of events, this waste adds up, leading to more cache misses and worse performance.

Fortunately, you can minimize padding by simply reordering the members. The optimal strategy is a greedy one: Declare members in decreasing order of their alignment requirement (which is usually their size).

Let's reorder our Example struct:

struct OptimizedExample {
    int  b;  // 4 bytes
    char a;  // 1 byte
    char c;  // 1 byte
};

Now, let's trace the layout:

  • int b; is placed at offset 0. Size: 4 bytes.
  • The next available address is offset 4.
  • char a; is placed at offset 4. Size: 1 byte.
  • The next available address is offset 5.
  • char c; is placed at offset 5. Size: 1 byte.
  • Total size is now 4 + 1 + 1 = 6 bytes.
  • Rule #2 check: Strictest alignment is 4. The next multiple of 4 after 6 is 8.
  • The compiler adds 2 bytes of tail padding.
  • Final sizeof(OptimizedExample) is 8 bytes.

By reordering, we reduced the size from 12 bytes to 8 bytes—a 33% improvement! This is a core technique in Data-Oriented Design.

Alignment and Packing

The article 'Alignment and Packing' from algorithmica.org reinforces this concept from a high-performance computing perspective.

Please read the sections 'Structure Alignment' and 'Optimizing Member Order'. They provide another clear example and confirm the rule of thumb: place larger data types first.

Test your understanding!

You are defining a struct to hold a log event for your telemetry system. The raw sum of member sizes is 24 bytes.

struct LogEvent {
    double timestamp;    // 8 bytes
    char   level;        // 1 byte
    int    thread_id;    // 4 bytes
    bool   is_error;     // 1 byte
    long   message_id;   // 8 bytes on 64-bit systems
    short  category;     // 2 bytes
};

Assuming a standard 64-bit system (sizeof(long) is 8, alignof(double) is 8, etc.), what is the sizeof(LogEvent) with this layout? Then, reorder the members to minimize padding and calculate the new, smaller size. Show the reordered struct definition.

Show answer

Original Layout Size:
Let's trace the layout with padding:

  • double timestamp (8 bytes): offset 0. Next is 8.
  • char level (1 byte): offset 8. Next is 9.
  • int thread_id (4 bytes): must align to 4. Next valid is 12. 3 bytes padding. thread_id at offset 12. Next is 16.
  • bool is_error (1 byte): offset 16. Next is 17.
  • long message_id (8 bytes): must align to 8. Next valid is 24. 7 bytes padding. message_id at offset 24. Next is 32.
  • short category (2 bytes): must align to 2. offset 32 is valid. Next is 34.
  • Total consumed: 34 bytes.
  • Strictest alignment is 8 (from double and long). The next multiple of 8 after 34 is 40. 6 bytes tail padding.
  • Original sizeof(LogEvent) is 40 bytes.

Optimized Layout:
Reorder members from largest to smallest.

struct OptimizedLogEvent {
    double timestamp;    // 8 bytes
    long   message_id;   // 8 bytes
    int    thread_id;    // 4 bytes
    short  category;     // 2 bytes
    char   level;        // 1 byte
    bool   is_error;     // 1 byte
};

Let's trace the new layout:

  • double timestamp (8 bytes): offset 0. Next is 8.
  • long message_id (8 bytes): offset 8. Next is 16.
  • int thread_id (4 bytes): offset 16. Next is 20.
  • short category (2 bytes): offset 20. Next is 22.
  • char level (1 byte): offset 22. Next is 23.
  • bool is_error (1 byte): offset 23. Next is 24.
  • Total consumed: 24 bytes.
  • Strictest alignment is 8. 24 is a multiple of 8, so 0 bytes tail padding are needed.
  • Optimized sizeof(OptimizedLogEvent) is 24 bytes.

By reordering, we eliminated all 16 bytes of padding, saving 40% of the memory for every single log event.


Conclusion

You now understand the crucial link between your C++ code and the underlying hardware. Memory alignment isn't just an obscure compiler detail; it's a fundamental aspect of performance engineering. For your telemetry engine, where you'll be creating and storing millions of data structures, arranging them efficiently is a primary optimization.

Key Takeaways:

  • Alignment: CPUs read memory in cache lines (e.g., 64 bytes). Data types are "aligned" when they are placed at memory addresses that are a multiple of their size, which prevents them from wastefully spanning two cache lines.
  • Padding: Compilers automatically insert unused bytes (padding) into structs to ensure every member is properly aligned.
  • Struct Size Rule: A struct's total size is padded to be a multiple of its most strictly aligned member to ensure alignment is maintained within arrays.
  • Optimization: You can minimize padding and memory usage by declaring struct members in descending order of size/alignment.

This lesson has laid the groundwork for our next topic. We've focused on how to arrange data within a single struct to be efficient. In the next lesson, we will zoom out and explain how CPU cache lines impact the performance of data access patterns across collections of objects. This will introduce you to critical performance concepts like cache locality and false sharing, which are direct consequences of the memory layout principles you learned today.