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.

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:
- Member Alignment: Each member must start at a memory address that is a multiple of its alignment requirement. A
charcan be anywhere (alignment of 1), but aninttypically needs to be at an address divisible by 4. - Struct Alignment: The total size of the
structitself must be a multiple of the alignment of its strictest (largest) member. In this case, theinthas an alignment of 4, so the total size ofExamplemust 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 arrayExample arr[10];, everyarr[i].bwill be correctly aligned.

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.
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_idat 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_idat 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
doubleandlong). 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.