c

Padding and alignment of C structs

Q: How to debug padding and alignment issues of C struct?

A: gdb --silent --batch -ex 'ptype /o struct my_t' some.o

The past days I was investigating some performance issues with a proprietary SoC: The code consists of closed-source pre-compiled binaries combined with public header files. Some public glue-code added accessors to allocate, copy, and free the data structures.

Padding

We have the requirement to extend the data-structure and add some additional members. As some code was close-sourced, it is important to not change the layout of the existing structure. Luckily C adds padding between members to align the next member according to common hardware constraints:

The start address of a 1,2,4,8,16,32,64,… sized member must align to that size.

Consider this:

#include <stdint.h>
struct my0_t {
    uint8_t foo;
    uint32_t bar;
} var0[3];
static_assert(sizeof(var0) == 8 * 3, "Unexpected sizeof");

bar has size 32 bits or 4 bytes, so var0[0] must be placed into memory so that &(var[0].bar) % 4 == 0 is true. The C-compiler will thus add padding bytes before bar to satisfy that requirement. Compiling the code with -Wpadded shows this:

$ gcc -c -g -Wpadded c-padding.c
c-padding.c:4:14: warning: padding struct to align ‘bar’ [-Wpadded]
    4 |     uint32_t bar;
      |              ^~~

But you don’t know, what the C compiler does here: gcc may either insert padding before foo or after it:

struct my1_t {
    uint8_t foo;
    uint8_t _padding[3];
    uint32_t bar;
} var1[3];
static_assert(sizeof(var1) == 8 * 3, "Unexpected sizeof");

or

struct my2_t {
    uint8_t _padding[3];
    uint8_t foo;
    uint32_t bar;
} var2[3];
static_assert(sizeof(var2) == 8 * 3, "Unexpected sizeof");

Both are valid, but all I have ever seen is padding being inserted after the previous member and before the next member.

But you can use gdbs ptype command to dump the exact layout including offset, size and inserted padding:

$ gdb --silent --batch -ex 'ptype /o struct my0_t' c-padding.o
/* offset      |    size */  type = struct my0_t {
/*      0      |       1 */    uint8_t foo;
/* XXX  3-byte hole      */
/*      4      |       4 */    uint32_t bar;
                               /* total size (bytes):    8 */
                             }

For this to work you need DWARF debugging information. So please make sure you compile your code with -g enabled!

This extra padding increases the size of your struct, which might be undesired: On embedded systems you often have less memory and excessive padding might waste a lot of memory. To minimize this, you have multiple options:

Packing

You can declare the struct as packed:

struct my3_t {
    uint8_t foo;
    uint32_t bar;
} __attribute__((packed)) var3[3];
static_assert(sizeof(var3) == 5 * 3, "Unexpected sizeof");

This makes the struct as compact as possible by not inserting any padding automatically. But you will get into trouble and risk getting a SIGBUS error on some architectures: Accessing a 32 bit variable which is not 4 byte aligned requires additional work:

  1. Either the hardware has some extra logic to split non-aligned memory access into multiple accesses and to recombine both parts into the final value,
  2. Or the compiler has to generate extra code to not do the unaligned access,
  3. Or your program terminates with SIGBUS as the processor raises the unaligned trap

Please do not use -fpack-struct to make every struct packed by default!

Re-ordering descending by size

Most often you can re-order your members descending by size – assuming sizes being a power-of-two. The compiler still adds padding, but only at the end of the structure. That way you do not have holes in the middle.

That changes the layout and breaks any ABI compatibility! So not not do this with structs, which are used to communicate with your hardware or some closed source binary, which assumes the old layout.

Careful re-ordering

If you only need to insert some small data, look for those hole: As the compiler added padding there automatically, there is no guarantee that these bits/bytes are zero initialized. If you need that guarantee, you must manually insert padding bytes!

On the other hand that provides the opportunity, to re-use those undefined bits for additional members. Just look for a hole which is large enough for your data and add your member in between the members bordering that hole.

Just be careful with structures which are used with hardware: If their accessor function does a memset(…, 0, …) to initialize the struct to zero, it might be important that those bits remain cleared. If you then start using those bits, the hardware might get confused.

Alignment

You might have noticed, that sizeof(struct my0_t) == 8 and not 5 == sizeof(uint8_t) + sizeof(uint32_t). gcc also adds padding before or after all members to extend the struct, until its sizeof if a natural multiple of the widest element. This is important for arrays where multiple instances are placed after each other. There each instances start address must be aligned properly, which requires padding in between. The distance between two elements is called “stride size”, which equals the sizeof.

This also applies to nested structs like this:

struct my5_t {
    struct my4_t baz;
    uint8_t bla;
} var5[3];
static_assert(sizeof(var5) == 12 * 3, "Unexpected sizeof");

This might be unexpected as my4_t ends with 3 padding bytes, where bla might fit it. Instead baz gets placed after the padding from baz, after which 3 more padding bytes are required. So in total you get 6 bytes of padding.

Cache line size

Alignment becomes even more important for performance. Modern CPUs have lots of caches and their line size specifies the smallest quantity for data transfer. Even when you only require a single bit, the cache will transfer 32 or 64 or even more bytes from RAM.

  • with more tightly packed structs you get more data per cache-line and require fewer cache-lines, leaving more free cache lines for other tasks.
  • on the other hand false sharing might become a performance issue with multi-threading, where data with different access patterns are stored in the cache line.
struct my6_t {
    uint8_t foo;
    uint32_t bar;
} __attribute__((aligned(32))) var6[3];
static_assert(sizeof(var6) == 32 * 3, "Unexpected sizeof");

In this case we get 3 bytes of padding between foo and bar. But we also get 24 bytes of padding after bar to make sizeof(struct my6_t) a multiple of 32 as requested by __attribute__((aligned(32))).

This easily becomes worse with nested ``structs where inner struct`s also have alignments:

struct my7_t {
    uint8_t foo;
    struct inner {
        uint8_t foo;
    } __attribute__((aligned(32))) bar[4];
} var7[3];
static_assert(sizeof(var7) == 64 * 3, "Unexpected sizeof");

Runnig gdb shows what happens:

$ gdb --silent --batch -ex 'ptype /o var7' c-padding.o 
type = struct my7_t {
/*      0      |       1 */    uint8_t foo;
/* XXX 31-byte hole      */
/*     32      |      32 */    struct inner {
/*     32      |       1 */        uint8_t foo;
/* XXX 31-byte padding   */
                                   /* total size (bytes):   32 */
                               } bar;
                               /* total size (bytes):   64 */
                             } [3]

Summary

  • Use gccs -Wpadded to get a warning.
  • Use gdbs ptype to print the real layout.
  • Verify your assumtions, especially if the same code is compiled for multiple platforms with different alignment requirements.
  • Do not trust the comments in the code claiming ancient values for sizeof or proper cache line alignment.
  • Explicitly add padding bytes as they are then also initialized; otherwise the compiler may do as it likes.

Further reading

Written on October 1, 2025