Code of the Day
AdvancedAdvanced pointers and memory

Memory alignment and padding

Understand why CPUs require aligned memory accesses, how the compiler pads structs, and how to measure and control struct layout.

CAdvanced10 min read
By the end of this lesson you will be able to:
  • Explain natural alignment and why CPUs require it
  • Predict struct padding using sizeof and offsetof
  • Reorder struct members to minimise padding
  • Use __attribute__((packed)) to eliminate padding when necessary

Modern CPUs do not access memory one byte at a time. They read and write data in aligned chunks — a 4-byte int must be at an address divisible by 4; an 8-byte double must be at an address divisible by 8. When a struct mixes types of different sizes, the compiler inserts padding bytes to satisfy these alignment requirements.

Natural alignment

A type's alignment is the boundary its address must be a multiple of. In practice:

TypeSizeAlignment
char11 (any address)
short22
int44
float44
double88
pointer8 (64-bit)8

Accessing an unaligned value is undefined behaviour on some architectures (bus error) and slower on others (the CPU may need two memory transactions to read a value that spans a cache line boundary).

Struct padding

The compiler ensures each member of a struct is properly aligned by inserting padding:

#include <stdio.h>
#include <stddef.h>

struct Example {
    char   a;   /* 1 byte at offset 0 */
                /* 3 bytes padding */
    int    b;   /* 4 bytes at offset 4 */
    char   c;   /* 1 byte at offset 8 */
                /* 7 bytes padding */
    double d;   /* 8 bytes at offset 16 */
};
/* sizeof(struct Example) = 24 */

Use offsetof (from <stddef.h>) to see the actual offset of each member:

printf("offset a = %zu\n", offsetof(struct Example, a)); /* 0 */
printf("offset b = %zu\n", offsetof(struct Example, b)); /* 4 */
printf("offset c = %zu\n", offsetof(struct Example, c)); /* 8 */
printf("offset d = %zu\n", offsetof(struct Example, d)); /* 16 */
printf("sizeof   = %zu\n", sizeof(struct Example));      /* 24 */

Minimising padding by reordering members

Reorder members from largest to smallest alignment:

struct Efficient {
    double d;   /* 8 bytes at offset 0 */
    int    b;   /* 4 bytes at offset 8 */
    char   a;   /* 1 byte at offset 12 */
    char   c;   /* 1 byte at offset 13 */
               /* 2 bytes padding to reach size 16 */
};
/* sizeof(struct Efficient) = 16 -- saves 8 bytes */

For hot data structures (accessed millions of times per second), smaller structs mean more fit in a cache line — directly impacting performance.

The __attribute__((packed)) escape hatch

For binary file formats and network protocols where you need exact byte layout without padding:

struct __attribute__((packed)) PacketHeader {
    uint8_t  type;
    uint16_t length;
    uint32_t sequence;
};
/* sizeof = 7, no padding */

Packed structs can cause unaligned accesses. On x86, unaligned reads/writes work but are slower. On ARM and RISC-V, they may bus-fault. If you access a packed struct member through a pointer, you may trigger an unaligned access. Only use __attribute__((packed)) for binary serialisation contexts, and access members through the struct directly (not through extracted pointers).

The C11 standard way: _Alignas and _Alignof

C11 provides standard keywords:

#include <stdalign.h>

_Alignas(16) double vec[4]; /* 16-byte aligned array (for SIMD) */

size_t align = _Alignof(double); /* 8 on most platforms */

Or using the macros:

alignas(16) double vec[4];
size_t align = alignof(double);

Custom alignment is critical for SIMD (Single Instruction Multiple Data) operations, where 128-bit, 256-bit, or 512-bit vectors must be aligned to their vector size.

Measuring and comparing layouts

#include <stdio.h>
#include <stddef.h>

#define SHOW(T, m) printf("  %-10s offset=%2zu size=%zu\n", \
                          #m, offsetof(T, m), sizeof(((T *)0)->m))

struct Bloated {
    char   a;
    double b;
    char   c;
    int    d;
    char   e;
};

int main(void) {
    printf("struct Bloated (size=%zu):\n", sizeof(struct Bloated));
    SHOW(struct Bloated, a);
    SHOW(struct Bloated, b);
    SHOW(struct Bloated, c);
    SHOW(struct Bloated, d);
    SHOW(struct Bloated, e);
    return 0;
}

This diagnostic pattern is useful when auditing structs for cache efficiency.

Where to go next

Next: restrict and aliasing rules — how the restrict keyword lets the compiler generate faster code and what the C aliasing rules say about pointer types.

Finished reading? Mark it complete to track your progress.

On this page