Ask a room of C programmers what sizeof this struct is and most answer 13:
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:
- Each field is placed at the next offset aligned to its own size.
- The struct’s total size is rounded up to its largest field’s alignment — so arrays of it stay aligned.
Apply them to order:
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 wastedSort 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:
$ 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 content — memcmp 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.