Skip to main content

Welcome to a Grasp lesson

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

Lesson 12

Optimizing Struct Memory Layout

Recommended resources

Hello! Welcome to your next lesson in the "Embedded Interview Preparation Bootcamp."

In our last lesson, we focused on const and volatile, two C qualifiers that act as directives to the compiler, controlling how it treats variables. We saw how const volatile is the professional way to handle read-only hardware registers.

Today, we'll shift our focus from compiler behavior to compiler layout. We'll investigate how the compiler arranges data in memory, a topic that is fundamental to embedded systems and a frequent subject in interviews for semiconductor companies.

This lesson directly addresses the learning outcome: Calculate structure size and padding manually, and rearrange struct members to minimize memory footprint.

1. Why Data Alignment Matters

Before we talk about structures, let's establish a core architectural principle: data alignment. Modern processors (like the ARM and x86 cores you'll encounter) don't access memory one byte at a time. They fetch data in larger, fixed-size chunks, often called "words" (e.g., 4 bytes on a 32-bit system, 8 bytes on a 64-bit system). Access is fastest when the data being fetched fits neatly within one of these chunks and starts at an address that is a multiple of its own size.

Understanding Memory: How to Use Structures in Embedded C

To understand the 'why' behind alignment, this article provides an excellent first-principles explanation of how processors access memory.

Please read the sections titled "Memory Access Granularity", "Memory Access Boundary", and "Data Alignment". These will explain why hardware prefers aligned data and establish the fundamental rule: a K-byte data type should be stored at a memory address that is a multiple of K.

When data is unaligned, the CPU might have to perform two separate memory fetches and then stitch the data together, which is significantly slower.

Data Alignment and CPU Processing Impact
This image illustrates why aligned memory access is more efficient. The unaligned integer and double span two processor words, requiring extra work for the CPU to read or write a single piece of data.

The C standard dictates these alignment requirements:

  • char (1 byte): Can be at any address.
  • short (2 bytes): Must be at an address divisible by 2.
  • int (4 bytes): Must be at an address divisible by 4.
  • long, double, pointers (often 8 bytes on 64-bit systems): Must be at an address divisible by 8.

2. Structure Padding: The How and Why

To enforce these alignment rules within a struct, the compiler inserts unused bytes called padding. This ensures that each member begins on its required memory boundary. This often makes the total size of the struct larger than the sum of its members.

C++ structure padding and packing | Memory alignment

Let's watch a video that introduces this concept with a simple, practical example. It clearly shows why the sizeof a struct can be surprising.

Watch the beginning of the video "C++ structure padding and packing". Introduction (0:00 - 1:46): This sets up the problem by showing a simple struct whose size is not what you'd naively expect. Alignment Rules (1:46 - 8:05): This section explains why the compiler adds padding and introduces the two most important rules for calculating struct size.

The Rules of Structure Padding

Based on the video and standard compiler behavior, we can distill the process into two key rules:

  1. Internal Padding: Each member of the struct must be placed at an offset from the start of the struct that is a multiple of its own alignment requirement. The compiler will insert padding bytes before a member to satisfy this.
  2. Trailing Padding: The total size of the entire struct must be a multiple of the alignment of its widest member. This ensures that in an array of these structs, every struct instance will start on a properly aligned boundary.
Struct Memory Layout with Padding on x86-64
This diagram provides a clear visual of a struct's memory layout on an x86-64 system. Notice the 3 bytes of padding after `char a` to align `int b`, and the 6 bytes of trailing padding to make the total struct size (24 bytes) a multiple of the widest member's alignment (8 bytes for `double c`).

Manual Calculation Exercise

Let's apply these rules to a typical interview-style problem. Given a 64-bit system (where pointers are 8 bytes), calculate the size of the following struct:

struct device_register {
    char    status;     // 1 byte
    long    device_id;  // 8 bytes
    int     config;     // 4 bytes
    short   checksum;   // 2 bytes
};

Let's walk through it:

  1. Struct Alignment: The widest member is long device_id (8 bytes). Therefore, the entire struct device_register must have an 8-byte alignment, and its total size must be a multiple of 8.

  2. Member Layout:

    • char status: Size 1. Placed at offset 0.
    • long device_id: Size 8. Needs an 8-byte aligned offset. The next available multiple of 8 is offset 8. The compiler inserts 7 padding bytes after status. device_id is placed at offset 8.
    • int config: Size 4. Placed at offset 16 (8 + 8). Its own alignment (4) is satisfied.
    • short checksum: Size 2. Placed at offset 20 (16 + 4). Its own alignment (2) is satisfied.
  3. Trailing Padding:

    • The last member, checksum, ends at offset 22 (20 + 2).
    • The total size must be a multiple of the struct's alignment (8).
    • The next multiple of 8 after 22 is 24.
    • The compiler adds 2 bytes of trailing padding.

Result: The sizeof(struct device_register) is 24 bytes. (1 + 7 + 8 + 4 + 2 + 2)


3. Structure Reordering: Minimizing Memory Footprint

As you saw, the order of members dramatically impacts the amount of padding. We can exploit this to save memory, which is critical in constrained embedded systems or when dealing with large datasets.

The general rule for optimization is simple: Declare structure members in order of decreasing alignment requirement.

The Lost Art of C Structure Packing

This article, "The Lost Art of C Structure Packing," is a classic resource that details the reordering technique and explains its significance.

Please read section 7, "Structure reordering". It provides a clear rule for minimizing padding and a great example using a linked-list node, showing a size reduction from 24 to 16 bytes. Pay attention to the explanation for why compilers don't do this automatically—it's a great interview talking point.

Reordering Exercise

Let's reorder our device_register struct and see the effect.

// Original struct (24 bytes)
struct device_register {
    char    status;
    long    device_id;
    int     config;
    short   checksum;
};

// Reordered struct
struct device_register_optimized {
    long    device_id;  // 8 bytes
    int     config;     // 4 bytes
    short   checksum;   // 2 bytes
    char    status;     // 1 byte
};

Let's recalculate:

  1. Struct Alignment: Still determined by long device_id (8 bytes).
  2. Member Layout:
    • long device_id: Size 8. Placed at offset 0.
    • int config: Size 4. Placed at offset 8. Alignment is satisfied.
    • short checksum: Size 2. Placed at offset 12. Alignment is satisfied.
    • char status: Size 1. Placed at offset 14. Alignment is satisfied.
    • No internal padding is needed!
  3. Trailing Padding:
    • The last member, status, ends at offset 15.
    • The total size must be a multiple of 8. The next multiple is 16.
    • The compiler adds 1 byte of trailing padding.

Result: The sizeof(struct device_register_optimized) is 16 bytes. By simply reordering the members, we achieved a 33% reduction in memory usage.


4. Advanced Case: Nested Structures

A common point of confusion, and thus a great interview question, is how padding works with nested structures. The key is that the alignment rule propagates upwards from the most-aligned member, wherever it may be.

Structural Padding In C & C++ Part 2

This next video tackles the nested structure problem directly. Understanding this is a key differentiator that demonstrates a deep grasp of memory layout.

Watch the key sections of "Structural Padding In C & C++ Part 2": Explanation (3:37 - 6:29): This part explains the core rule: the compiler "opens up" the inner struct to find the true widest member, which then determines the alignment for the outer struct. Reordering Example (7:28 - 9:55): This powerful example shows how placing an integer between a nested struct and another member can drastically increase the total size, highlighting the importance of member placement.

The takeaway is that the alignment of an outer struct is determined by the alignment of the widest scalar member anywhere inside it, including within nested structs.


Conclusion

Mastering data alignment and structure padding is non-negotiable for a senior embedded role. It demonstrates an understanding that goes beyond just writing code to understanding how that code interacts with the hardware.

Key Takeaways:

  • Alignment is for Performance: Compilers add padding to ensure that each data member is naturally aligned, allowing the CPU to access it with the fewest memory operations.
  • Calculate with Two Rules:
    1. Add internal padding so each member starts at an offset that is a multiple of its own alignment.
    2. Add trailing padding so the struct's total size is a multiple of its widest member's alignment.
  • Optimize by Reordering: You can significantly reduce a struct's memory footprint by ordering members from largest to smallest alignment requirement.
  • Nested Structures: The alignment of a nested struct is determined by the widest scalar member within it, and this influences the layout of the parent struct.

Preview of the Next Lesson:

While reordering is a powerful technique for saving memory, sometimes you need to do the opposite: force a specific, non-optimal memory layout. This is common when your struct needs to exactly match a hardware register block or a network protocol packet definition. In our next lesson, we will learn how to do this by exploring compiler-specific directives: Apply #pragma pack and __attribute__((packed)) to control data alignment and explain the performance trade-offs.