Layoutpaddingstruct layoutalignmentABIcache lines

Padding: Why Your Struct Is Bigger Than Its Fields

A struct with a char and an int is not five bytes. The compiler inserts padding to keep every field naturally aligned, and in a large array of those structs the padding is memory you pay to move but never read.

▶ Run the labFollow the mechanism

Software view, hardware view

The gap between what you wrote and what the machine does is where this whole domain lives.

The question
Why does `sizeof` a struct exceed the sum of its fields, and when is that worth caring about?
What you wrote
A struct is its fields laid end to end. Field order is a stylistic choice with no runtime meaning.
What the hardware does
Each field is placed at an offset satisfying its alignment, with padding bytes inserted to get there and trailing padding added so arrays of the struct stay aligned. Field order therefore determines size.
In a large array, padding is dead weight that consumes cache lines and memory bandwidth without carrying information. Reordering three fields can shrink a struct by a third and produce a real, measurable speedup in a scan — one of the few pure wins in performance work.
SourceCompilerInstructionsFront EndExecutionRegistersCachesMemoryI/OBehavior

Where the bytes go

The classic example is struct { char a; int b; }. The char occupies one byte at offset 0. The int requires 4-byte alignment, so it cannot start at offset 1 — the compiler inserts three padding bytes and places it at offset 4. Total: 8 bytes for 5 bytes of data.

There is a second, less obvious source. Trailing padding is added so that the struct's size is a multiple of its strictest field alignment, which guarantees that in an array, element i+1 is aligned exactly like element i. A struct containing an 8-byte double and a single char is 16 bytes, not 9.

None of this is the compiler being wasteful. It is the compiler honouring the platform ABI, which fixes these rules so that separately compiled code agrees on layout. Which also means the layout is ABI-specific: the same source can lay out differently on a different platform.

struct { char a; int b; } — 5 bytes of data, 8 bytes on the wire
usedpaddingABI-SPECIFIC
a (char)paddingb (int)
line 0
8 bytes total1 cache line touched3 bytes of padding

Three of eight bytes carry no information. In an array of a million of these, that is three megabytes of cache and bandwidth spent moving nothing.

Field order is a performance decision

Because padding is inserted to reach the next valid offset, ordering fields from largest alignment to smallest minimises it. The same four fields can produce a 24-byte struct or a 16-byte struct depending purely on declaration order, with identical semantics.

Whether this matters depends entirely on how the struct is used. For a handful of instances it is irrelevant and reordering purely for size is premature. For an array of millions scanned in a hot loop it is one of the highest-leverage changes available, because it directly determines how many elements fit in a cache line — and therefore how many misses the scan takes.

The rule of thumb worth internalising: struct size matters in proportion to how many of them you have and how often you sweep them. Everywhere else, declare fields in whatever order makes the code clearest.

Declaration order interleaves sizes: 24 bytes
1struct Record {
2 char flag // offset 0, 1 byte
3 // offset 1, 7 bytes padding
4 double value // offset 8, 8 bytes
5 char category // offset 16, 1 byte
6 // offset 17, 7 bytes trailing padding
7}
8// sizeof == 24, of which 14 bytes are padding
9// a 64-byte cache line holds 2 records (with 16 bytes spare)
Largest alignment first: 16 bytes
1struct Record {
2 double value // offset 0, 8 bytes
3 char flag // offset 8, 1 byte
4 char category // offset 9, 1 byte
5 // offset 10, 6 bytes trailing padding
6}
7// sizeof == 16, of which 6 bytes are padding
8// a 64-byte cache line holds 4 records
9
10// Same fields, same semantics, 33% less memory moved per scan.

Nothing about the program's meaning changed — only the order of declarations. But a sequential scan over a large array now touches half as many cache lines, which for a bandwidth-bound scan is close to a straight doubling of throughput. This is why field ordering appears in performance guidance despite feeling like a stylistic detail.

Packing, and why it is usually the wrong tool

Most languages offer a way to eliminate padding entirely — #pragma pack, __attribute__((packed)), #[repr(packed)]. It does what it says, and it creates exactly the problem Alignment: Why Addresses Are Not Arbitrary describes: fields end up misaligned, so access becomes slower on tolerant architectures and faults on strict ones. Taking a reference to a packed field is a hard error in some languages precisely because the resulting pointer would be unaligned.

The legitimate use is matching an externally defined layout: a wire protocol, a file format, a memory-mapped hardware register block. There, the layout is not yours to choose and packing expresses a real constraint. Even then, the safer pattern is often to read and write fields explicitly with memcpy rather than declaring a packed struct and dereferencing it.

For performance, reordering fields is almost always the better answer than packing. It costs nothing at runtime, breaks no alignment rules, and often recovers most of the space that packing would.

Three ways to shrink a struct, and what each costs
TechniqueSavesCosts
Reorder fields, largest alignment firstMost internal paddingNothing at runtime; declaration order may read less naturally
Use narrower types where the range allowsReal data bytesRange and precision; risk of overflow — see Integer Overflow: The Hardware Wraps, the Language Decides
Pack the structAll paddingMisaligned fields: slower on tolerant ISAs, faults on strict ones, unsafe references
Split hot and cold fields into separate arraysBytes the hot loop never readsLoses locality when a single record needs all fields — see Array of Structs, or Struct of Arrays?

Key points

  • Padding is inserted to keep each field naturally aligned, and trailing padding keeps array elements aligned to each other.
  • Field declaration order therefore determines struct size, with no change to semantics.
  • Ordering fields from largest alignment to smallest minimises padding and can shrink a struct substantially.
  • Size matters in proportion to how many instances exist and how often they are scanned — irrelevant for a few, decisive for millions.
  • Packing removes padding but reintroduces misalignment; reordering is the better tool unless an external format dictates layout.

Struct Layout & Padding

Change an input and watch which number moves — and which one refuses to.

Field order decides the size
Declared in a natural reading order
usedpaddingABI-SPECIFIC
padint bpaddouble d
line 0
24 bytes total1 cache line touched10 bytes of padding

Each field must sit at an address that is a multiple of its size, so the compiler inserts padding to get there. Twenty-four bytes to hold fourteen bytes of data, and in an array of a million records that is ten megabytes of nothing.

Follow the mechanism

The path through the machine, hop by hop — and the conclusions it invites that are wrong.

  1. 1
    Compiler → field offsets: each field is placed at the next offset satisfying its ABI-defined alignment, inserting padding to reach it.
  2. 2
    Compiler → struct size: trailing padding rounds the size up to a multiple of the strictest member alignment.
  3. 3
    Array → memory: elements are laid end to end at that size, so padding is replicated once per element.
  4. 4
    Scan → cache: each cache line brings in a fixed number of bytes, of which the padding fraction carries no information.
  5. 5
    Bandwidth → throughput: for a bandwidth-bound scan, throughput scales roughly with the useful fraction of each line.
What people conclude from this — wrongly
  • "The compiler wastes memory." It is honouring the ABI so that separately compiled code agrees on layout; the alternative is misaligned access.
  • "I should pack every struct." Packing trades a space win for misaligned access, which is slower where tolerated and fatal where not.
  • "Field order is purely stylistic." It determines size, which determines records per cache line, which determines miss count in a scan.

Consequences, controls and cost

What it causes
  • • A large array of poorly ordered structs consumes noticeably more memory and bandwidth than necessary.
  • • The number of records per cache line changes with field order, directly affecting miss counts in a scan.
  • • Struct layout differing between platforms breaks any code that assumes a byte layout without declaring one.
What you can do
  • • Order fields from largest alignment to smallest in structs that appear in large arrays.
  • • Use narrower types where the value range genuinely permits, being careful about overflow.
  • • Split frequently accessed fields from rarely accessed ones when scans only touch a few — the [[aos-vs-soa]] transformation.
  • • Reserve packing for externally defined layouts, and prefer explicit byte-level read and write over dereferencing packed fields.
How to see it
  • • Print `sizeof` and each field offset and compare against the sum of field sizes; the difference is padding.
  • • Use a layout-dumping compiler flag or a struct-layout tool to see the actual placement rather than inferring it.
  • • Benchmark a scan over a large array before and after reordering, watching cache misses rather than just wall time.
What it costs
  • • Reordering for packing can separate logically related fields, hurting readability for a win that only matters at scale.
  • • Narrower types risk overflow and often need explicit conversion, adding instructions to save bytes.

Scope

§224 — what these claims are specific to.

What these claims are specific to
  • ABI-SPECIFICAlignment requirements and therefore exact offsets are set by the platform ABI. The same source lays out differently between, for example, 32-bit and 64-bit targets.
  • SIMPLIFIEDAssumes a C-like layout where declaration order is preserved. Some languages, notably Rust with its default representation, are free to reorder fields automatically and often already pack optimally.

Misconceptions

Claim
“sizeof(struct) equals the sum of sizeof(field).”
Reality
Only when no padding is needed. Internal padding aligns fields and trailing padding aligns array elements, so the sum is a lower bound rather than the answer.
Claim
“Padding is wasted memory the compiler could avoid.”
Reality
Avoiding it means misaligned fields, which the ABI forbids and which cost more than the bytes saved on most architectures.
Claim
“Reordering fields is a micro-optimisation.”
Reality
For a struct instantiated a handful of times, yes. For an array of millions scanned in a hot loop it changes how many records fit per cache line, which is a first-order effect.

Apply it

Where the rest of this lives

Programming Languages & Runtime Internals
Object headers and language-controlled layout

Managed runtimes add object headers and may reorder or box fields, so the layout a JVM or CLR object gets is decided by the runtime rather than by declaration order the way a C struct is.