The problem › Locality is decided at declaration time
You chose your miss rate when you wrote the struct
Chapter 0.6 showed that the order you visitbytes swings performance by an order of magnitude. But you don't always get to choose the visiting order: the algorithm chooses it. What you always choose is the order the bytes are laid down in the first place. Declaring a struct, picking array-of-structs versus struct-of-arrays, even sequencing fields: each is a locality decision, made before the first instruction runs, that determines which bytes share the 64-byte lines of chapter 0.5. The cache line is the smallest unit of data that moves between cache and memory, so cache-friendly layout means thinking in lines, not variables.
The mechanism › Alignment and padding
Types are picky about their addresses
First, a rule the compiler enforces behind your back: any primitive object of K bytes must sit at an address that is a multiple of K. A char can live anywhere; a short wants a multiple of 2; an int or float a multiple of 4; a long, double, or pointer a multiple of 8. The reason is fetch granularity: a processor that always reads aligned 8-byte chunks can service any aligned double with one memory operation. Let the value straddle two chunks (recall the fence-post arithmetic of 0.1) and one access becomes two.
A subtlety worth knowing: on x86-64, unaligned data still works; the hardware handles it, and alignment is Intel's performance recommendation, not a correctness rule. The historical exception was SSE: its 16-byte vector loads faulted on unaligned addresses, which is why malloc and stack frames guarantee 16-byte alignment to this day.
Predict first
struct { char a; double b; int c; }: the fields total 1 + 8 + 4 = 13 bytes. What does sizeof report?
Both padding rules come straight from the compiler's contract: gaps inside the struct keep each field aligned, and trailing padding keeps every element of an array of the struct aligned. The fix costs one line of diff: declare fields in decreasing size order. Put b at 0, c at 8, a at 12: every field lands aligned with 3 tail bytes instead of 11. Bakhvalov shows the same transformation shrinking a {bool; int; short} struct from 12 bytes to 8.
Size order is not the only ordering that matters. When a struct is bigger than one line, group the fields that are used together: if the hot loop reads attack, defense, and health, but health was declared 64 bytes away, that phase of the program pays two cache lines per object instead of one. Reordering so co-accessed fields share a line is the same locality argument at field granularity, and modern tooling (Linux perf mem data-type profiling) exists precisely to find these opportunities.
SourcesCS:APP 3e §3.9.3 · pp. 309–312Bakhvalov 2e §8.1.3–8.1.4 · pp. 195–197
The mechanism › AoS vs SoA
One array of structs, or one struct of arrays
Padding is the small version of the layout decision. The big one: you have a million particles, each with position, velocity, and mass. Array-of-structs(AoS) keeps each particle's fields together: natural to write, and right when you use all fields of a particle at once. Struct-of-arrays (SoA) keeps one array per field: all the masses contiguous, all the x-positions contiguous.
Now run a pass that reads only the mass, one 4-byte field of a 24-byte struct. In AoS, a mass appears every 24 bytes, so every 64-byte line of the whole array contains one: the walk drags all 24 MB of struct data through the cache to consume 4 MB of masses. In SoA, the masses are the byte run: 4 MB touched, 4 MB used, the pure stride-4 row-walk from 0.6. Same computation, 6× less memory traffic, from a declaration-site choice.
AoS: mass every 24 B, every line fetched
SoA: masses contiguous, only 1/6 of the lines fetched
The practitioner's middle ground is structure splitting: carve the hot fields into their own compact struct and banish the cold ones to a second array. Splitting Point into PointCoords + PointInfo means a coordinates-only pass loads nothing but coordinates: more points per cache line, no rewrite of the whole program into SoA. And when even the fields themselves are bigger than their information content, packing (bitfields) trades a few shift-and-mask instructions for fewer bytes moved, worth it exactly when compute is cheaper than the memory transfers it saves, which after 0.4 you know is most of the time.
This exact picture is why ML frameworks store tensors as struct-of-arrays (a tensor is SoA taken to its logical conclusion), and it is the idea that becomes GPU coalescing in chapter 2.4, where 32 threads read 32 neighboring values and the hardware fuses them into a handful of line fetches, but only if the layout put those values adjacent.
SourcesBakhvalov 2e §8.1.3–8.1.5 · pp. 195–198CS:APP 3e §6.5 · p. 669
The number › Your turn
Price the padding, then reorder it away
Compute the number › sizeof, by hand, both orderings
| declared { char a; double b; int c; } | 1 + 8 + 4 = 13 B of data |
| a at 0, b needs 8-alignment | +7 padding → b at 8 |
| c at 16, ends at 20 | +4 tail padding (align 8) |
| sizeof, as declared | 24 B (46% packaging) |
| reordered { double b; int c; char a; } | b at 0, c at 8, a at 12 |
| sizeof, reordered | 13 + 3 tail = 16 B |
| Result | 24 B → 16 B: reordering deletes 33% of the footprint |
Across a million-element array that's 24 MB shrunk to 16 MB, from a diff that changes no logic whatsoever. Fewer bytes, fewer lines, fewer misses: the whole hierarchy thanks you in proportion. (CS:APP's practice problem 3.45 asks exactly this: rearrange the fields to minimize wasted space.)
interactive · coming soon
StructPacker: drag fields, watch the lines
Planned interactive: drag struct fields into different orders and watch offsets, padding, sizeof, and the cache-line footprint of a million-element array update live.
The full story › false sharing, the multicore booby trap
Layout has one more trick waiting once multiple cores enter (a future aside, but worth the teaser). Two threads updating two different variables that happen to share one 64-byte line will fight over it: the coherence protocol tracks ownership per line, not per variable, so each core's write yanks the line away from the other's cache, and the "independent" counters crawl as if they were one contended lock. Ostrovsky's gallery (example 6) measures the effect; the cure is the same discipline as this whole chapter: know which bytes share a line, and pad hot per-thread data to a line boundary on purpose. Padding as a feature, not a tax.
Layout is locality decided in advance: alignment pads your structs (declare fields largest-first, group what's accessed together), and AoS vs SoA decides whether a walk over one field drags six lines or one. The bytes you place together are the bytes you'll pay to move together. Choose like it matters, because in Topic 2 the GPU will grade you on it.