Part 3 · Reading compiler output
Structs, padding and layout
Field order changes how big your struct is. Not by a little, and not in a way you can guess without knowing the rule.
What this lesson is trying to break (3)
- A struct is the sum of its fields.
- Field order is a style choice.
- Rust structs lay out in declaration order.
Alignment is a machine constraint, not a language one, and it produces bytes you did not ask for.
The rule
Two clauses:
- Each field sits at an offset that is a multiple of its own alignment. For
primitives, alignment equals size: a 4-byte
intmust be at an offset divisible by 4. - The struct’s total size is a multiple of its largest alignment.
Clause 2 is the one people forget, and it exists for arrays: if sizeof(S) were
not a multiple of S’s alignment, then arr[1] would be misaligned even though
arr[0] was fine.
struct A { char a; int b; char c; };
struct B { int b; char a; char c; };Same three fields, different order. What is sizeof each?
A is 12. B is 8.
struct A struct B
offset 0: char a (1 byte) offset 0: int b (4 bytes)
offset 1: PADDING (3 bytes) offset 4: char a (1 byte)
offset 4: int b (4 bytes) offset 5: char c (1 byte)
offset 8: char c (1 byte) offset 6: PADDING (2 bytes)
offset 9: PADDING (3 bytes) ─────────────────────────────
───────────────────────────── total: 8
total: 12In A, the int cannot start at offset 1, so three bytes are wasted. Then the
trailing char needs three more bytes of tail padding to round the struct to a
multiple of 4.
In B, the int goes first where it is already aligned, and both chars pack
into the same word. Only two bytes of tail padding.
A third of A is padding. Reordering the declaration — a change with no
semantic effect whatsoever — removed 4 bytes, one third of the size. In a
million-element array that is 4 MB, and more importantly it changes how many
elements fit in a cache line, which is
4.4’s subject.
The rule of thumb that falls out: declare fields largest-first. It is not always optimal but it is rarely bad.
Seeing it without a debugger
You can read layout straight out of the assembly. Compile this:
struct S { char a; int b; char c; };
int get_b(struct S *s) { return s->b; }
int size(void) { return sizeof(struct S); }
size returns a constant — the compiler folded sizeof — so it tells you the
total directly. And get_b shows you the offset: you will see a load from
[rdi+4], which says b lives at offset 4, which tells you three bytes of
padding preceded it.
That is a genuinely useful technique. Given a binary and no headers, field offsets are readable from the loads that access them.
Rust does something different
Rust makes no layout guarantee for ordinary structs. It is free to reorder fields, and it does, precisely to minimise padding. So:
struct A { a: u8, b: u32, c: u8 } // Rust: 8 bytes, reordered internally
Rust gives you the packed layout automatically, where C makes you earn it by declaring in the right order.
The cost of that freedom is that you cannot depend on the layout — which is what
#[repr(C)] is for:
#[repr(C)]
struct A { a: u8, b: u32, c: u8 } // 12 bytes, C rules, declaration order
You need #[repr(C)] whenever layout is observable from outside: FFI, memory-mapped
hardware, wire formats, anything you transmute. Using it unnecessarily means
opting back into padding you did not need.
#[repr(C)] · #[repr(packed)] · #[repr(transparent)]
repr(C)— C layout and declaration order. Something outside Rust sees this type.repr(packed)— no padding at all. Fields may be misaligned, so reads may be slower or, on some targets, trap. References to misaligned fields are undefined behaviour, which is whypackedis genuinely dangerous rather than merely unusual.repr(transparent)— a single-field wrapper with identical layout to its field. The newtype pattern’s guarantee that it costs nothing.
You have a struct used in an array of ten million elements, currently 24 bytes, of which 7 are padding.
Give two independent reasons shrinking it to 17 (or 20 after tail padding) would speed up a loop that scans the array — one about memory volume and one about cache lines. Then say why the second reason usually matters more.
What you should now be able to say
- Fields must be at offsets that are multiples of their alignment, and struct size must be a multiple of its largest alignment.
- Field order changes
sizeof— a third of a badly-ordered struct can be padding. Declare largest-first. - Field offsets are readable from load instructions;
sizeoffolds to a constant. - Rust reorders to minimise padding;
#[repr(C)]opts out for FFI and observable layout;repr(packed)removes padding and can cause UB.
Next: 3.6 — Verifying “zero-cost”.