Ask a room of C programmers what sizeof this struct is and most answer 13:

c
struct order {
    char     status;    /* 1 byte  */
    double   price;     /* 8 bytes */
    int      qty;       /* 4 bytes */
};

It’s 24. Nearly half the struct is padding — invisible bytes the compiler inserted, silently, on every one of the fifty million instances your service keeps in memory. The rules that put them there fit on an index card, and knowing them is one of the highest-leverage habits in systems C: it regularly turns into double-digit memory savings for the cost of reordering lines.

The Rules

Hardware prefers (and on some architectures, requires) that an N-byte scalar live at an address divisible by N — a misaligned load can mean two memory transactions, a slow microcoded fixup, or on stricter targets (older ARM, and atomics everywhere) a fault. So the compiler enforces:

  1. Each field is placed at the next offset aligned to its own size.
  2. The struct’s total size is rounded up to its largest field’s alignment — so arrays of it stay aligned.

Apply them to order:

text
  offset 0    status (1 byte)
  offset 1-7  ── 7 bytes padding ──   (price needs offset % 8 == 0)
  offset 8    price  (8 bytes)
  offset 16   qty    (4 bytes)
  offset 20-23 ── 4 bytes padding ──  (round size to multiple of 8)
                                       total: 24 bytes, 11 wasted

Sort fields largest-first and the waste evaporates: price(0), qty(8), status(12), pad 3 → 16 bytes. Same fields, one-third smaller — and in a hot array, one-third fewer cache misses, because layout compounds with the 64-byte-line economics: 4 structs per line instead of 2.66.

Don’t audit by eye; the kernel community’s tool does it better:

bash
$ pahole -C order ./a.out
struct order {
        char   status;   /*  0  1 */
        /* XXX 7 bytes hole, try to pack */
        double price;    /*  8  8 */
        int    qty;      /* 16  4 */
        /* size: 24, holes: 1, padding: 4 */
};

pahole reads DWARF debug info and annotates every hole in every struct in the binary — run it over a codebase once and it pays for the afternoon. (The kernel does this routinely; struct layout reviews are a normal part of patches touching hot types.)

Packing: The Attribute with Teeth

__attribute__((packed)) deletes all padding — and it’s the right tool in exactly one situation: matching an externally defined byte layout (wire protocols, file formats, memory-mapped hardware registers) where the layout is contract, not choice. As a memory optimization it’s a trap: every access to a misaligned member becomes potentially-slow byte-assembly code, taking a pointer to a packed member is UB-adjacent (&pkt->seq may not satisfy int*’s alignment contract — GCC warns with -Waddress-of-packed-member), and atomics on misaligned fields are broken outright. Even for wire formats, the more defensible pattern is explicit serialization — memcpy fields to/from a byte buffer, which the compiler turns into the same loads/stores without the landmines, and which handles endianness at the same place.

Two more layout facts worth keeping loaded: bitfields are the other packing mechanism, with implementation-defined ordering — fine for flags inside one program, wrong for parsing wire headers portably (the network stack’s #if __BYTE_ORDER dance around IP header bitfields is the cautionary tale). And padding bytes have indeterminate contentmemcmp of structs is a bug generator, and structs copied to disk/network/userspace leak stack garbage through the holes (a recurring class of kernel infoleak CVEs; hence memset before fill in every driver you’ve read).

At the far end, alignment goes from correctness to performance tool: alignas(64) to give a hot mutex or per-CPU counter its own cache line (false-sharing insurance), page-aligned buffers for O_DIRECT, and SIMD-friendly 32/64-byte alignment for vectorized kernels.

Takeaways

  • Two rules generate all padding: fields align to their size, structs round to their largest member. Largest-first ordering is usually optimal.
  • Run pahole; holes in hot structs are free memory and free cache hits waiting to be claimed.
  • packed is for matching external layouts, not saving memory — misaligned access costs, pointer hazards, broken atomics.
  • Never memcmp padded structs; never ship them across trust boundaries without memset — padding is indeterminate and leaks.
  • Alignment is also a weapon: alignas(64) isolates contended fields; bitfield order is not portable.