1 · Memory and the CPU Draft
Hardware review and further reading
On this page
1.11 What to be able to say afterwards
- At 4 GHz an L1 hit costs about 4 cycles and a DRAM miss about 360, so a 1 µs software budget affords only about eleven dependent DRAM misses.
- Count cycles for anything inside the core and nanoseconds only at the packet edges: the clock moves, a clock read costs 20–30 ns, and the TSC ticks at a nominal rate, not the core’s.
- The cache moves 64-byte lines, so an operation’s cost is set by the number of distinct lines it touches, not the number of bytes it reads.
- A 48 KB L1 is 64 sets of 12 ways; addresses 4 KB apart share a set, so a power-of-two stride can miss in a cache that is mostly empty.
- Out-of-order execution hides L1 and L2 latency and overlaps independent misses, but it cannot hide a chain of dependent DRAM misses.
- Layout should follow access: struct of arrays for scans over one field, array of structs for updates to one element, and hot fields split from cold ones.
- Threads writing different variables in one cache line serialise through the coherence protocol, so each line should have one writer and few cross-core readers.
- A predictable branch is nearly free, a 50/50 branch costs 8–10 cycles on average, and branch-free code helps only in the second case.
- On a two-socket machine memory is placed by first touch, so a hot thread must be pinned and must initialise its own data.
- Latency estimates and examples depend on the target; check Rust’s layout guarantees before treating a measured size as a contract.
- Every performance claim needs a prediction first and a pinned, release-build, percentile-reported measurement second.
1.12 Quiz
- Your hot path performs six dependent DRAM misses per message. What does that cost when the memory is local, and when it is on the remote NUMA node? How does each compare with a 1 µs software budget?
- Take
#[repr(C)] struct S { a: u8, b: u64, c: u8 }. What aresize_of::<S>()andalign_of::<S>()? What would Rust’s default layout give? How much memory does an array of one million take under each? - A feed handler executes
if side == Side::Bidon 5 million messages per second, with bids and asks in random order. The mispredict penalty is 18 cycles at 4 GHz. How much CPU time per second goes to mispredicting this branch? Is it the first thing to fix? - Per-instrument state lives in a
Vec<InstrumentState>whose elements are exactly 4,096 bytes. On every tick you readbest_bidat offset 0 for 40 instruments. The L1d is 48 KB, 12-way, with 64 sets. What happens, and what are two fixes? - Lab 1.1 shows about 15 ns per access at a 4 KB stride, where every access misses to DRAM. Lab 1.4 shows about 120 ns per node for the shuffled list, where every access also misses to DRAM. Explain the eightfold difference.
- An SPSC ring has
headandtailon separate padded lines, but the producer reads the sharedtailon every push. At 10 million messages per second, with a cross-core line transfer of about 70 ns, estimate the producer’s coherence cost per second. Then estimate the best batching case with a cachedtailon a 1,024-slot ring, and explain why a consumer keeping up does not guarantee it.
Answers
- Local: 6 × 90 ns = 540 ns, over half the budget before any computation. Remote: 6 × 175 ns ≈ 1,050 ns, over budget on memory alone. The same code on the wrong socket breaks the design, which is why first-touch placement matters.
- With
repr(C)on this x86-64 target:aat 0, 7 padding bytes,bat 8,cat 16 and 7 tail-padding bytes, so size 24 and alignment 8. A typical default Rust layout is 16 bytes, giving 24 MB versus 16 MB for a million elements; verify it withsize_of, because the default layout is unspecified. - Half mispredict: 5 M × 0.5 × 18 = 45 million cycles, about 11 ms per second, roughly 1% of a core, or about 2 ns per message. That is small next to one 90 ns DRAM miss, so fix the misses first. Mispredicts dominate when a branch runs many times per message, as in a byte-by-byte parser.
- Elements 4,096 bytes apart share address bits 6–11, so all 40 lines map to one 12-way set. Each is evicted before reuse, so every read misses L1 although the lines total 2.5 KB. Pad the struct to 4,160 bytes, or move
best_bidinto its own denseVec<u64>. - Stride addresses come from a loop counter, so the core keeps eight or more misses in flight: roughly 120 ns ÷ 8 ≈ 15 ns each. Each list address comes from the previous load, so one miss is in flight at a time, each paying full latency plus a TLB miss. Lab 1.1 measures throughput under memory-level parallelism; Lab 1.4 measures latency.
- If every read needs a transfer, the upper-bound estimate is 10 M × 70 ns = 0.7 s per second before overlap. If each refresh grants 1,024 pushes, about 9,766 refreshes cost 0.68 ms per second. The second figure is a batching scenario, not a guarantee from capacity or a consumer merely keeping up; actual occupancy and interleaving decide the saving. Slot and publication traffic remains.
Further reading
- Ulrich Drepper, “What Every Programmer Should Know About Memory” (2007): section 3 on caches and section 6 on what programmers can do.
- Agner Fog, Optimizing software in C++, The microarchitecture of Intel, AMD and VIA CPUs and the instruction tables (latencies in cycles), all free at agner.org/optimize.
- Wikipedia as the first stop for the mechanisms in section 1.1: Out-of-order execution, Tomasulo’s algorithm, Branch predictor and the associativity section of CPU cache.
- Fabian Giesen, “A whirlwind introduction to dataflow graphs” (2018), on dependency chains, and “Cache coherency primer” (2014), the shortest good explanation of MESI.
- The coherence protocols themselves: MESI, Intel’s MESIF and AMD’s MOESI on Wikipedia.
- Henry Wong, “Measuring Reorder Buffer Capacity” (2013): the reorder-buffer window measured from user code.
- Dan Luu, “Branch prediction” (2017): how predictors evolved, with the trade-offs at each step.
- On timing: the Time Stamp Counter article and the RDTSC instruction reference, for why
rdtscdeltas are reference ticks rather than core cycles. - Mara Bos, Rust Atomics and Locks (O’Reilly, 2023), chapters 1–2, on threads, sharing and atomics in Rust.
- Carl Cook, “When a Microsecond Is an Eternity: High Performance Trading Systems in C++”, CppCon 2017.
- Brendan Gregg, “Linux perf Examples” (brendangregg.com/perf.html), for practical
perf statandperf recordrecipes. - Jeff Dean, “Numbers Everyone Should Know”, the original latency ladder; compare its figures with your own measurements.
- The
crossbeam_utils::CachePaddeddocumentation on docs.rs, including its per-architecture alignment choices.