# Struct Padding: The Bytes You're Paying For but Not Using

> Reorder three fields and a struct shrinks from 24 bytes to 16 — a 33% memory cut with zero code changes. Where padding comes from, how to see it with pahole, and when packing is a trap.

URL: https://harshith.in/blog/struct-padding-and-alignment/
Author: Harshith Sunku (Staff Engineer) — https://harshith.in/about/
Tags: memory, performance, systems

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 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.
