The Numbers
A working reference for every constant this journey depends on: where it comes from, how it was obtained, what assumptions hold it up, what breaks it, and — the part most reference cards omit — which decision it actually changes.
This is not a table to memorise. It is a set of derivations you should be able to reconstruct. A number you can only recall is a number you will misapply the moment the hardware, the dtype, or the access pattern changes.
Table of Contents
- How To Read This
- The Reference Machine
- 1. The Latency Hierarchy
- 2. Memory Bandwidth
- 3. Boundary Crossings
- 4. Concurrency Primitives
- 5. Arithmetic Throughput
- 6. The Interpreter Tax
- 7. Transformer Arithmetic
- 8. Retrieval and ANN
- 9. Storage Engines
- 10. Distributed Systems
- 11. Statistics and Experimentation
- 12. Recommendation Systems
- 13. Floating Point
- 14. How These Numbers Lie
- The One-Page Table
- References
How To Read This
Reproduce all of it first. Everything in sections 1–5 comes from one shipped program:
cd tools
cc -O2 -o machine-baseline machine-baseline.c
./machine-baseline # the tables below, on your machine
./machine-baseline --json > mine.json
python3 baseline.py mine.json # diff vs the reference + 8 invariant checks
baseline.py does not just diff. It checks eight invariants that must hold on any
machine — a monotone hierarchy, a syscall costing ≥50× an empty loop, fsync ≥100× a
syscall — and tells you which conclusions in this track still apply to you if one fails.
A failed invariant almost always means a broken measurement rather than exotic hardware.
Every figure carries one of three labels. The label is load-bearing; a plan that treats a reported number as a measured one is how you end up optimising against a fiction.
| Label | Meaning |
|---|---|
| measured | Produced by running a named script on the reference machine. Reproducible. Will differ on yours — the shape will not |
| derived | Computed from measured quantities or from an identity. The arithmetic is shown so you can check it |
| reported | From a datasheet or paper. Not verified here. Treated with suspicion, and the suspicion is documented |
And every entry answers four questions:
- What is the number?
- How was it obtained — with the derivation, not just the result.
- What assumption holds it up, and what makes it wrong?
- What decision does it change? A number that changes no decision is trivia.
The Reference Machine
Everything labelled measured comes from this machine. Record yours in
notebook/001-machine-baseline.md in week one
and re-derive the ratios; the absolute values will move by 2–5× and almost none of the
conclusions will.
platform : macOS 15.0, arm64 (Apple Silicon), 12 cores
python : CPython 3.14.0, numpy 2.4.6 (BLAS = Apple Accelerate)
compiler : clang, -O2 unless stated
load : NON-IDLE. loadavg ~4.3 during several runs.
Tail latencies here are pessimistic; medians are close to right.
filesystem: APFS on the internal SSD (/dev/disk3s5)
The load average is stated on purpose. A benchmark taken on a machine doing other work is not wrong, it is conditioned — and a reader who does not know that will misread every p99 on this page. See §14.
1. The Latency Hierarchy
Script: a pointer-chase over a single random cycle, one pointer per 64-byte line. measured
| Working set | ns/dependent load | Level | Cycles @ 3.5 GHz |
|---|---|---|---|
| 4 KB | 0.89 | L1d | 3.1 |
| 32 KB | 0.91 | L1d | 3.2 |
| 64 KB | 0.92 | L1d | 3.2 |
| 128 KB | 0.91 | L1d (still!) | 3.2 |
| 192 KB | 5.08 | L2 | 17.8 |
| 256 KB | 5.64 | L2 | 19.7 |
| 1 MB | 5.94 | L2 | 20.8 |
| 4 MB | 5.85 | L2 | 20.5 |
| 8 MB | 7.00 | L2 / SLC edge | 24.5 |
| 16 MB | 14.00 | transition | 49.0 |
| 32 MB | 69.08 | DRAM-ish | 241.8 |
| 64 MB | 97.85 | DRAM | 342.5 |
| 128 MB | 112.66 | DRAM | 394.3 |
| 512 MB | 121.10 | DRAM | 423.9 |
The derivation that matters
\[ \text{L1} : \text{L2} : \text{DRAM} \;=\; 0.91 : 5.94 : 121.10 \;=\; \mathbf{1 : 6.5 : 133} \]
Memorise the ratio, not the nanoseconds. The ratio has been roughly 1 : 10 : 100 for twenty years across wildly different hardware, because it is set by physics and economics (SRAM area per bit, distance to the memory controller, DRAM row-activation time) rather than by any vendor's choices. The absolute numbers halve every few years; the ratio does not move.
Why the measurement must be a random cycle
A dependent load chain is required: each load's address comes from the previous load's result, so the CPU cannot overlap them. Without that you measure throughput (several loads in flight) rather than latency.
A random cycle is required on top of that. My first attempt used a fixed 64-byte stride, which is exactly sequential cache lines — the hardware prefetcher recognises it instantly and every "miss" is already in flight. That version reported 1.30 ns at 512 MB, i.e. it claimed DRAM was as fast as L1. Sattolo's algorithm (a single random cyclic permutation) fixed it. Details in §14.
The 128 KB L1 is unusual and worth noticing
Most x86 cores have a 32–48 KB L1d. This machine holds 0.91 ns out to 128 KB, which is an Apple performance-core design choice. Consequence for you: any blocking or tiling constant you tune here is not portable. When P14 tells you to predict the optimal matmul block size from your L1 size, this is the number you predict from — and it is 4× the value you would assume from an x86 background.
What decisions this changes
- P14 tiling. The optimal block size is the largest \(B\) with \(3B^2 \times 4\text{ bytes} \le \text{L1}\). At 128 KB: \(B \le \sqrt{128{,}000/12} = 103\). Measured optimum was 32 — which is 3× smaller than the cache-capacity bound, because vector-register pressure and loop overhead bind first. That gap between the naive capacity prediction and the measured optimum is exactly the kind of thing you only learn by measuring.
- P12 context-switch cost. A switch that evicts a 1 MB working set costs \(16{,}384 \text{ lines} \times 121\text{ ns} \approx 2\,\text{ms}\) of subsequent refill in the worst case — three orders of magnitude more than the 1.7 µs switch itself. This is why E4 sweeps working-set size, and why a ping-pong benchmark understates switching so badly.
- P02 ANN. 100k vectors × 128 dims × 4 B = 51 MB — comfortably DRAM-resident, so brute force runs at bandwidth, not at latency (it is a streaming scan, fully prefetchable). The graph walk is the opposite: random pointer chasing at ~121 ns per hop. The graph trades a prefetchable scan for a dependent-load chain, and that is a second, independent reason it loses at small \(n\) beyond the interpreter overhead.
- Any data structure choice. A linked list traversal is a dependent-load chain; an array scan is prefetchable. At 133× the difference, this dominates asymptotic complexity for anything under ~10⁴ elements.
2. Memory Bandwidth
Script: sequential sum / store over a 512 MB buffer. measured
| Operation | GB/s |
|---|---|
| Sequential read | 57.5 |
| Sequential write | 62.9 |
| Read + write (both directions counted) | 99.1 |
Latency × bandwidth: the concurrency you must sustain. By Little's Law (\(L = \lambda W\)), to sustain 57.5 GB/s at 121 ns of latency you need
\[ L = 57.5 \times 10^{9}\,\text{B/s} \times 121 \times 10^{-9}\,\text{s} = 6{,}958\ \text{bytes in flight} \approx 109\ \text{cache lines} \]
derived. So the memory system must keep ~109 line-fills outstanding at all times to reach peak. A single dependent-load chain keeps exactly one in flight, which is why pointer chasing achieves \(64\,\text{B} / 121\,\text{ns} = 0.53\) GB/s — 108× below peak. Same hardware, same DRAM, two orders of magnitude apart, decided entirely by whether the accesses are independent.
This one calculation explains: why prefetching exists, why SoA beats AoS, why batching helps, why GPUs need thousands of threads, and why "our database is slow and the CPU is idle" is almost always a dependent-load problem.
Arithmetic intensity and the ridge point
For any kernel doing \(W\) FLOPs while moving \(Q\) bytes from DRAM:
\[ I = W/Q \quad\text{[FLOP/byte]}, \qquad \text{perf} = \min(P,\; I \times B), \qquad I_{\text{ridge}} = P/B \]
derived, from tools/roofline.py:
| Hardware | Dense peak | Bandwidth | Ridge | Note |
|---|---|---|---|---|
| H100 SXM | 989.4 TF/s | 3,350 GB/s | 295 F/B | reported. 1979 TF/s is the 2:4 sparse figure — do not use it for a dense GEMM |
| A100 80GB | 312 TF/s | 2,039 GB/s | 153 F/B | reported. 624 TF/s is sparse |
| Server CPU | 5.1 TF/s | 307 GB/s | 17 F/B | reported, AVX-512 fp32, 8ch DDR5-4800 |
| This machine | ~1.7 TF/s | 57.5 GB/s | ~29 F/B | derived from the measured BLAS peak and read bandwidth |
The asterisk is the point. Vendor headline FLOP/s are routinely quoted with 2:4 structured sparsity, doubling the number for a workload that does not apply to dense matmul. Use the sparse figure as your denominator and every efficiency you report is halved. Check the footnote every time.
3. Boundary Crossings
measured (C, -O2), except where noted.
| Operation | Cost | In L1 hits | Note |
|---|---|---|---|
| Empty loop iteration | 0.31 ns | 0.34 | The measurement floor |
getpid() | 1.23 ns | 1.4 | NOT a syscall — libc caches the pid |
clock_gettime(MONOTONIC) | 18.00 ns | 20 | Also not a syscall — served from a shared page |
close(-1) — real trap, fails immediately | 127.59 ns | 140 | A genuine user→kernel→user round trip |
| Pipe round trip, 2 processes | 3,272–3,864 ns | ~3,900 | 4 syscalls + 2 context switches |
| ⇒ implied context switch | 1,383–1,706 ns | ~1,680 | \((\text{rt} - 4\times128)/2\), derived |
write(4K) + fsync | 90–105 µs | ~107,000 | ~9,500–11,000 durable writes/s. measured, and real — fsync must reach the device |
Two of these are given as ranges, and that is the honest form. Across four runs of
machine-baseline on the same machine the pipe round trip varied by 18% and fsync by
16%, because both are scheduler- and device-sensitive in ways the pointer chase is not.
Quoting 1,676 ns to four significant figures — as an earlier draft of this page did —
implies a precision the measurement does not have. Run it three times and report the
spread; a single run of a load-sensitive benchmark is an anecdote.
The getpid() trap, in full
getpid() at 1.23 ns is four times an empty loop iteration. A mode switch cannot
cost four instructions; the number is physically impossible for a syscall. glibc and
Apple's libc cache the pid in userspace after the first call, so you are timing a
function call and a load.
Essentially every "syscalls only cost a nanosecond" claim on the internet traces to this benchmark. It is the single most common microbenchmarking error in systems work, and it is instructive precisely because the result looks plausible if you do not know the floor.
The fix: use a syscall that must trap and that fails immediately, so you measure the
boundary and not the work. close(-1) is ideal: it validates the descriptor, fails, and
returns. clock_gettime is not a valid choice either — 18 ns is too fast for a trap,
because it is served from a shared kernel/user page (the commpage on macOS, the vDSO on
Linux) precisely to avoid one.
What the numbers mean structurally
\[ \text{loop iteration} : \text{syscall} : \text{context switch} : \text{fsync} \;=\; 1 : 412 : 5{,}406 : 290{,}000 \]
derived. Five orders of magnitude across four operations you routinely treat as similar. Consequences:
- A syscall is ~140 L1 hits. Doing one per 4-byte read is why unbuffered I/O is
catastrophic and why
io_uring,sendmmsg, and vectored I/O exist. It is also whyclock_gettimewas moved out of the kernel — at 128 ns a timestamp in a hot loop would be a visible cost, and at 18 ns it is merely annoying. - A context switch is ~13 syscalls — in the best case. This is a ping-pong benchmark with a tiny working set and warm caches. The dominant real cost is cache and TLB pollution, which does not appear here at all. See §1 for the ~2 ms worst case.
- fsync is ~700 DRAM round trips. At ~10,900 durable writes/second, a fsync-per-write storage engine is capped at 10,900 writes/s regardless of everything else you do. That single number is why group commit exists, and it is the ceiling P04 measures itself against in E10.
A caution on the reads. The sequential-read and random-4K-read figures I measured on
this machine are page-cache numbers, not device numbers — 14 GB/s sequential and
1.06M IOPS at 4K both exceed what any consumer SSD can do, and the first exceeds the
DRAM bandwidth in §2. fcntl(F_NOCACHE) on APFS is advisory and
did not evict. macOS has no O_DIRECT. I therefore report no device-level read
numbers; measure yours in P04 milestone 1
with a working-set several times RAM, and state your methodology. The write+fsync figure
survives because fsync has to reach durable media to return.
4. Concurrency Primitives
measured, single thread, uncontended.
| Operation | Cost | vs relaxed atomic |
|---|---|---|
volatile long++ | 0.33 ns | — |
atomic_fetch_add, relaxed | 2.01 ns | 1.0× |
atomic_fetch_add, seq_cst | 3.97 ns | 2.0× |
pthread_mutex_lock + unlock | 6.38 ns | 3.2× |
malloc(64) + free | 18.71 ns | 9.3× |
malloc(64K) + free | 18.48 ns | 9.2× |
Reading these correctly
Sequential consistency costs exactly 2× relaxed here. The difference is the memory
barrier: seq_cst must order this operation against all others, which on arm64 means a
dmb ish (or the ldaddal acquire-release form) that prevents the store buffer from
being drained lazily. Relaxed atomics only guarantee atomicity of the read-modify-write
itself. Decision this changes: a statistics counter incremented on every request
should be relaxed — you want the count, not an ordering guarantee — and choosing
seq_cst by default doubles its cost for nothing.
An uncontended mutex is only 3.2× a relaxed atomic, which is far cheaper than most people assume. Modern mutexes are a compare-and-swap on the fast path and never enter the kernel unless contended. The catastrophic case is contention: once a waiter has to sleep, you pay a context switch (~1,676 ns, 260× the uncontended lock) plus the cache effects. Decision this changes: "avoid locks, they're slow" is wrong as stated. Avoid contended locks. An uncontended mutex around a rare operation is free; a spinlock on a hot cache line shared by 8 threads is a disaster. This is exactly what P12's E9 measures.
malloc is flat from 64 B to 64 KB — both ~18.6 ns. Small allocations come from a
per-thread size-class cache with no locking; 64 KB still fits below the mmap threshold.
The number jumps sharply once the allocator must call mmap, which is a syscall
(≥128 ns) plus page-fault costs on first touch. Decision this changes: allocation is
~20 ns, i.e. ~22 L1 hits, i.e. not free in an inner loop but nowhere near the cost of
a syscall. Pooling matters at 10⁷ allocations/s, not at 10⁵.
5. Arithmetic Throughput
Single-threaded C, \(N = 512\) fp32 matmul, \(2N^3 = 268\) MFLOP. measured.
| Variant | -O2 | -O3 -ffast-math -mcpu=native |
|---|---|---|
naive i,j,k | 1.91 GF/s | 3.14 GF/s |
loop-reordered i,k,j | 27.33 GF/s | 27.27 GF/s |
| blocked, B=16 | 10.06 | 46.44 |
| blocked, B=32 | 15.16 | 58.30 GF/s |
| blocked, B=64 | 21.64 | 38.29 |
| blocked, B=128 | 26.15 | 33.60 |
| Accelerate BLAS (AMX) | — | 1,679 GF/s |
Four separate lessons, all in one table
1. Loop order alone is 14.3× (1.91 → 27.33, same flags, same arithmetic, same
instruction count). i,j,k strides B by \(N\) — a new cache line every inner
iteration — while i,k,j walks B contiguously. derived: at \(N=512\), the i,j,k
inner loop touches \(512 \times 64\text{B} = 32\) KB of B per i,j pair with zero
reuse across j, so it streams the whole matrix from L2/DRAM \(N\) times.
2. At -O2, blocking is worse than plain reordering (26.15 vs 27.33 at the best
block size) and only becomes worth 2.1× once the inner loop vectorises. An
optimisation's value is conditional on which other optimisations are present. Measure
one in isolation and you will conclude it is useless when it is not — and this is a
completely general trap, not a matmul quirk.
3. The optimum block size is 32, not the capacity bound of 103. Register pressure and loop overhead bind before L1 capacity does.
4. Hand-written peaks at 58.3 GF/s against Accelerate's 1,679 — a 28.8× gap. That is not sloppy code. Accelerate dispatches to Apple's AMX matrix coprocessor: dedicated silicon with its own register file and a systolic datapath. This is P14's entire thesis, measurable on a laptop before writing a line of the project. Verified across sizes (N=256: 1,008 GF/s; N=512: 1,679; N=1024: 1,705; N=2048: 1,316) with fp32-consistent relative error ~10⁻⁷, so it is real arithmetic and not a lazy evaluation artifact.
The systolic-array derivation
derived, and worth doing by hand once. A \(k \times k\) array of multiply-accumulate cells, weight-stationary, activations flowing horizontally and partial sums vertically:
- operands fetched per cycle: \(O(k)\) (one column)
- MACs performed per cycle: \(k^2\)
- operand reuse: \(O(k)\)
At \(k = 256\) (TPUv1): \(2 \times 65{,}536 \times 700\times10^6 = 91.75\) TOPS, against the reported 92 TOPS. You can derive a real accelerator's headline number from two integers. The cost is total inflexibility — no branches, no gather, one operation — which is the same restriction-buys-performance trade as MapReduce's programming model, implemented in silicon.
6. The Interpreter Tax
CPython 3.14.0, timeit, min of 5 repeats. measured.
| Operation | ns | In L1 hits |
|---|---|---|
pass (loop overhead floor) | 8.28 | 9 |
| local variable read | 10.20 | 11 |
| global variable read | 10.43 | 11 |
try/except (no raise) | 10.24 | 11 |
| int add | 12.79 | 14 |
obj.x via __slots__ | 12.82 | 14 |
| tuple unpack | 13.06 | 14 |
obj.x (instance dict) | 13.12 | 14 |
list[0] | 14.95 | 16 |
| list comprehension, per element | 16.23 | 18 |
| float add | 17.69 | 19 |
dict['k'] | 19.45 | 21 |
isinstance() | 21.68 | 24 |
| function call | 24.73 | 27 |
| method call | 25.77 | 28 |
| string concat | 27.65 | 30 |
numpy.float32 scalar add | 34.43 | 38 |
next(generator) | 37.16 | 41 |
| f-string | 45.74 | 50 |
raise + catch ValueError | 86.92 | 96 |
| numpy add, 1-element array | 254.56 | 280 |
| numpy add, 1M array (per element) | 0.2551 | 0.28 |
numpy.dot, 1M (per element) | 0.0136 | 0.015 |
The dispatch crossover, derived
numpy array addition costs ~254.56 ns of fixed overhead plus ~0.2551 ns per element. Overhead equals element work at
\[ n^{*} = \frac{254.56}{0.2551} \approx \mathbf{998\ \text{elements}} \]
Below ~1,000 elements, a numpy operation is more dispatch than arithmetic. At \(n = 1\) you pay 254 ns to perform one addition that costs 0.26 ns — a 998× overhead ratio. This is the single most important number in P13 Phase II, and it is why operator fusion and graph mode exist: they amortise dispatch across more arithmetic.
It is also why numpy.dot at 0.0136 ns/element is 18.8× cheaper per element than
+ at 0.2551 — the dot product is one BLAS call over a contiguous buffer with FMA and
vectorisation, whereas elementwise add is memory-bound at 3 arrays × 4 bytes = 12 bytes
per 1 FLOP (\(I = 0.083\) FLOP/byte, far below the ~29 ridge).
Python vs C, quantified
| Python | C equivalent | Ratio | |
|---|---|---|---|
| dict lookup | 19.45 ns | ~1–5 ns hash+probe | ~4–20× |
| function call | 24.73 ns | ~1–2 ns | ~15–25× |
| float add | 17.69 ns | ~0.3 ns | ~60× |
| attribute access | 13.12 ns | ~0.9 ns (L1 load) | ~15× |
The measured 67.5× per-distance ratio in P02 (899 ns interpreted vs 13.3 ns BLAS) is exactly this tax, compounded because each distance involves a function call, an attribute access, a numpy dispatch, and a heap operation. That is the mechanism behind "my algorithm is right and my implementation is wrong."
Object sizes — the memory tax
measured, sys.getsizeof:
| Object | Bytes |
|---|---|
float | 24 |
int | 28 |
| tuple (empty) | 48 |
__slots__ instance, 2 attrs | 48 |
| list (empty) | 56 |
| dict (empty) | 64 |
| set (empty) | 216 |
plain instance + __dict__, 2 attrs | 344 |
__slots__ is a 7.2× reduction (344 → 48 bytes) for a two-attribute object.
Decision this changes: in P09 you simulate 10⁵–10⁶ user objects. At 344 B that is
344 MB before any data; at 48 B it is 48 MB. The difference decides whether the simulator
fits in memory, and it is one line of code.
Also note a float is 24 bytes for 8 bytes of payload — 3× overhead — which is why a
Python list of 10⁶ floats costs ~32 MB (56 + 8 B/pointer + 24 B/object) against numpy's
4 MB for fp32. 8×, before any speed consideration.
7. Transformer Arithmetic
derived, for \(B{=}1, H{=}12, d_h{=}64\) (so \(d_{model}=768\), GPT-2 small). Per layer, forward, 2 FLOPs per MAC.
| Component | FLOPs | Scaling |
|---|---|---|
| Q,K,V projections | \(3 \cdot 2BTd^2\) | linear in \(T\) |
| \(QK^\top\) | \(2BHT^2 d_h\) | quadratic |
| scores·V | \(2BHT^2 d_h\) | quadratic |
| output projection | \(2BTd^2\) | linear |
| FFN (4× expansion) | \(16BTd^2\) | linear |
| T | Attention GF | FFN GF | Quadratic share | Score matrix (fp32) |
|---|---|---|---|---|
| 128 | 0.65 | 1.21 | 2.7% | 0.8 MB |
| 512 | 3.22 | 4.83 | 10.0% | 12.6 MB |
| 1024 | 8.05 | 9.66 | 18.2% | 50.3 MB |
| 2048 | 22.55 | 19.33 | 30.8% | 201.3 MB |
| 4096 | 70.87 | 38.65 | 47.1% | 805.3 MB |
| 8192 | 244.81 | 77.31 | 64.0% | 3,221 MB |
At GPT-2's original 1024 context, attention is 18% of the compute. The quadratic term does not dominate until ~4096. Most "attention is the bottleneck" claims are context-length-dependent and wrong for the context they are made about.
Memory hits the wall long before FLOPs do: 3.2 GB for a single batch element's score matrix at \(T=8192\). This is the entire motivation for FlashAttention — the compute is identical, the materialisation is what is avoided.
RoPE, verified
measured. The defining property is that \(\langle \text{RoPE}(q,m), \text{RoPE}(k,n)\rangle\) depends only on \(m-n\):
m n m-n dot
5 3 2 -8.115791933
105 103 2 -8.115791933
7 5 2 -8.115791933
1000 998 2 -8.115791933
5 4 1 -8.519139825
50 49 1 -8.519139825
Identical to nine decimal places across a 200× range of absolute position. Norm preserved exactly (7.315343549 → 7.315343549), because rotations are orthogonal. Write both as tests before implementing — they catch the two standard bugs (wrong dimension pairing, and rotating values as well as queries/keys).
Decode is bandwidth-bound, and the identity that proves it
Per generated token, every weight is read once and shared across the batch:
\[ W = 2Nb, \qquad Q = N \cdot \text{bytes}, \qquad I = \frac{2Nb}{N\cdot\text{bytes}} = \frac{2b}{\text{bytes}} \]
Arithmetic intensity in decode depends on batch size and nothing else — not on model
size, not on kernel quality. derived, from tools/roofline.py,
7B params bf16 on H100:
| Batch | I (F/B) | Step (ms) | tok/s | MFU | Regime |
|---|---|---|---|---|---|
| 1 | 1.0 | 4.179 | 239 | 0.3% | memory-bound |
| 8 | 8.0 | 4.179 | 1,914 | 2.7% | memory-bound |
| 64 | 64.0 | 4.179 | 15,314 | 21.7% | memory-bound |
| 256 | 256.0 | 4.179 | 61,257 | 86.7% | memory-bound |
| 512 | 512.0 | 7.245 | 70,671 | 100% | compute-bound |
A batch-1 chatbot uses 0.3% of an H100's multipliers. The step time is identical from batch 1 to 256 because you are paying to move 14 GB of weights either way. This single table justifies continuous batching, explains why serving economics are what they are, and gives the batch needed to leave the memory-bound regime:
\[ b^{*} = I_{\text{ridge}} \times \text{bytes} / 2 \]
= 295 (bf16, H100), 153 (bf16, A100), 148 (fp8, H100). Quantising to fp8 halves the required batch because it halves \(Q\) while leaving \(W\) unchanged — the real reason low precision wins at inference is smaller operands, not faster arithmetic.
8. Retrieval and ANN
Relative contrast predicts difficulty; dimension does not
measured, uniform on the unit sphere, \(n = 10{,}000\), \(\mathrm{RC} = d_{\text{mean}}/d_1\):
| d | RC | \(d_{10}/d_1\) |
|---|---|---|
| 16 | 2.220 | 1.2303 |
| 64 | 1.356 | 1.0717 |
| 128 | 1.224 | 1.0465 |
| 512 | 1.097 | 1.0196 |
At \(d = 512\) the tenth neighbour is 2% further than the first. Nothing can reliably distinguish them, so an ANN benchmark on uniform high-dimensional data measures the dataset, not the index. Always report RC alongside recall.
The clustering trap, measured
A Gaussian perturbation with per-axis \(\sigma\) in \(d\) dimensions has expected norm \(\sigma\sqrt{d}\). Around unit-norm cluster centres:
| σ | σ√d (d=64) | RC |
|---|---|---|
| uniform | — | 1.356 |
| 0.25 | 2.00 | 1.393 ← indistinguishable from uniform |
| 0.15 | 1.20 | 1.608 |
| 0.10 | 0.80 | 1.992 |
| 0.05 | 0.40 | 3.371 |
Verify your independent variable actually varies before spending a run on it. Ten minutes of checking saved a worthless experiment here.
The two-factor speedup model
The most transferable diagnostic in the whole journey. derived, validated twice:
\[ \text{speedup} = \underbrace{\frac{n}{\text{dists/query}}}_{\text{algorithmic}} \Big/ \underbrace{\frac{\text{ns/dist}_{\text{graph}}}{\text{ns/dist}_{\text{brute}}}}_{\text{constant factor}} \]
| Dataset | Algorithmic | Constant factor | Predicted | Measured |
|---|---|---|---|---|
| uniform (RC 1.36) | 10,000/1,459 = 6.9× | 899 / 13.3 = 67.5× | 0.10× | 0.10× |
| clustered (RC 3.36) | 10,000/321 = 31.1× | 1317 / 12.8 = 102.9× | 0.30× | 0.30× |
Exact to two significant figures in both cases. It says something specific: the algorithm is right and the implementation is wrong — a different bug with a different fix than "the algorithm is wrong." Break-even in pure Python lands near \(n \approx 1.5\times10^5\); in compiled code the constant factor is ~1–2× and the crossover falls to a few thousand. This is why every serious ANN index is C++.
The clustered recall ceiling
measured, \(n{=}10\)k, \(d{=}64\), M=16, efC=100:
| efSearch | uniform recall@10 | clustered recall@10 | uniform dists/q | clustered dists/q |
|---|---|---|---|---|
| 10 | 0.3605 | 0.4840 | 397 | 193 |
| 64 | 0.8160 | 0.8345 | 1,459 | 321 |
| 128 | 0.9480 | 0.9030 | 2,484 | 501 |
| 256 | 0.9930 | 0.9670 | 4,059 | 840 |
Clustered is faster everywhere and worse above ef≈96. The clustered search cannot spend its budget — 840 distances at ef=256 versus 4,059. Mechanism: intra-cluster distance 0.521, inter-cluster 1.413 (measured, 20k sampled pairs), so a distance-based degree cap deletes every inter-cluster bridge deterministically. Full analysis in the worked notebook entry.
Filtered search over-fetch
derived. Post-filtering with selectivity \(s\) needs \(K \ge k/s\) candidates:
| s | K for k=10 | % of a 1M corpus |
|---|---|---|
| 0.1 | 100 | 0.01% |
| 0.01 | 1,000 | 0.10% |
| 0.001 | 10,000 | 1.00% |
| 0.0001 | 100,000 | 10.00% |
At \(s = 10^{-4}\) you are running efSearch ≥ 100,000, which is brute force with a
worse constant. This is the production p99 cliff on narrow filters. And it
understates the problem: independence between filter and similarity fails whenever the
filtered attribute correlates with position in embedding space, and in the adversarial
case no \(K\) suffices. Meanwhile brute force over the filtered set is \(O(sn)\) — at
\(s=10^{-4}\) on 1M vectors that is 100 distance computations, exact, and faster than
either alternative.
9. Storage Engines
The three amplifications
derived, fanout \(T = 10\), 64 MB base level.
Leveled: \(W \approx TL+1\), \(R \approx L+1\), \(S \approx 1 + 1/T\). Size-tiered: \(W \approx L+1\), \(R \approx TL\), \(S \approx 2\).
| Data | Levels | Leveled W / R / S | Size-tiered W / R / S |
|---|---|---|---|
| 1 GB | 2 | 21 / 3 / 1.10 | 3 / 20 / 2.11 |
| 8 GB | 3 | 31 / 4 / 1.10 | 4 / 30 / 2.11 |
| 64 GB | 3 | 31 / 4 / 1.10 | 4 / 30 / 2.11 |
| 512 GB | 4 | 41 / 5 / 1.10 | 5 / 40 / 2.11 |
Read this as a choice, not a ranking. Leveled writes each byte ~31× to keep reads at 4 tables and space at 1.1×; size-tiered writes ~4× and pays with 30 tables per read and 2.1× the disk. On write-heavy ingest size-tiered is ~8× cheaper in device wear; on read-heavy serving leveled is ~7× cheaper in seeks. There is no third option that wins both — that is the RUM conjecture with numbers attached.
Bloom filters
Optimal \(k = (m/n)\ln 2\), giving \(\text{fpr} = 0.6185^{m/n}\). derived, and measured against 100k keys / 200k absent probes:
| bits/key | k | Fill ratio | Theory | Measured | RAM/100k keys |
|---|---|---|---|---|---|
| 4 | 3 | 0.5275 | 0.14689 | 0.14709 | 0.05 MB |
| 8 | 6 | 0.5281 | 0.02158 | 0.02204 | 0.10 MB |
| 10 | 7 | 0.5042 | 0.00819 | 0.00822 | 0.12 MB |
| 16 | 11 | 0.4973 | 0.00046 | 0.00047 | 0.20 MB |
Theory matches within 5%. Fill ratio sits at 0.5 at every optimum — that is the entropy argument appearing in the data: at the optimum each bit carries maximum information.
Read amplification for an absent key with 40 runs on disk (derived):
| bits/key | fpr | Disk reads | Improvement |
|---|---|---|---|
| none | 1.0 | 40.0 | 1× |
| 4 | 0.147 | 5.88 | 7× |
| 10 | 0.00819 | 0.328 | 122× |
| 16 | 0.00046 | 0.018 | 2,179× |
125 KB of RAM per 100k keys turns 40 disk reads into 0.33. That is why 10 bits/key is the near-universal default in RocksDB, LevelDB and Cassandra — and now you can derive it rather than cite it.
Measurement resolution caveat, and it generalises. At 24 bits/key the predicted fpr is ~10⁻⁵, so 200k probes expect 1.2 false positives. Observing 0 or 3 is Poisson noise, not a result. To measure a rate \(p\) you need ~\(100/p\) trials for a ~10% relative standard error — so 10⁻⁵ needs ~10 million probes. State the resolution of your experiment before reporting any ratio.
10. Distributed Systems
Quorum intersection
Two quorums of size \(Q\) from \(N\) replicas intersect iff \(2Q > N\), i.e. \(Q \ge \lfloor N/2 \rfloor + 1\). If \(2Q \le N\), two disjoint quorums can each decide without seeing the other. Split-brain is not an implementation bug; it is this inequality being violated — most often by an operator changing the replica count.
derived, \(N=3\), independent failure probability \(p\):
| p | Single node | 2-of-3 quorum | Downtime/year |
|---|---|---|---|
| 0.01 | 99.00% | 99.9702% | 3.65 d → 2.6 h |
| 0.05 | 95.00% | 99.2750% | 18.25 d → 15.9 h |
| 0.10 | 90.00% | 97.2000% | 36.5 d → 10.2 d |
The assumption doing all the work is independence, and it is usually false: same rack, same power domain, same bad deploy, same poisoned request. Correlated failure collapses this table entirely. Say so in any availability claim you make.
The tail at scale
If a request touches \(N\) independent components each exceeding its p99 1% of the time, \(P(\text{at least one slow}) = 1 - 0.99^N\). derived:
| N | P(≥1 slow) |
|---|---|
| 1 | 1.00% |
| 10 | 9.56% |
| 100 | 63.40% |
| 1000 | 99.996% |
At 100 components the majority of requests hit a p99 event. Tail latency is not an edge case at scale, it is the common case — and it is why fan-out architectures need hedged requests, and why "our p50 is fine" tells you nothing.
Stragglers
Job completion is a maximum over tasks, and maxima behave badly. Simulated, 200 tasks of 10 s on 20 workers, greedy list scheduling, 2,000 trials (derived):
| Scenario | Completion | vs ideal | With backup tasks |
|---|---|---|---|
| no stragglers | 100.00 s | 1.00× | — |
| 1% at 10× | 112.45 s | 1.12× | 108.64 s (1.09×) |
| 5% at 10× | 149.87 s | 1.50× | 110.01 s (1.10×) |
| 1% at 50× | 448.20 s | 4.48× | 108.64 s (1.09×) |
Two tasks out of two hundred inflate the job 4.5×. Backup tasks recover almost all of it. That is why MapReduce §3.6 exists, and it is far more persuasive as a table you generated than a sentence you read.
11. Statistics and Experimentation
Sample size
\[ n_{\text{per arm}} = \frac{2(z_{1-\alpha/2} + z_{\text{power}})^2\sigma^2}{\delta^2} \]
At α=0.05, power=0.8: \((1.96 + 0.8416)^2 = 7.849\), which is where the folklore "16σ²/δ²" comes from. derived, σ=0.5:
| MDE δ | n per arm |
|---|---|
| 0.05 | 1,570 |
| 0.025 | 6,280 |
| 0.0125 | 25,117 |
Exactly 4× per halving of the effect. Do this before proposing an experiment: if your realistic effect is 0.5% and you get 20,000 users a week, the experiment needs a year and should not be run. This calculation kills more proposals than any other number here.
Peeking
Simulated A/A tests, 4,000 trials, 500 users per look, stop at first significance (derived):
| Looks | False-positive rate |
|---|---|
| 1 | 5.12% |
| 2 | 8.58% |
| 5 | 14.47% |
| 10 | 19.30% |
| 20 | 24.15% |
| 50 | 32.80% |
Checking a dashboard daily for a fortnight turns a 5% error rate into ~25%. One in four "wins" is noise. Not a subtlety — the largest single source of false results in industrial experimentation.
Sample-ratio mismatch
χ² with 1 d.f.; alarm at p < 0.001, i.e. χ² > 10.83 (derived):
| Split | χ² | Verdict |
|---|---|---|
| 200,000 / 200,000 | 0.000 | ok |
| 201,000 / 199,000 | 10.000 | ok (just) |
| 202,000 / 198,000 | 40.000 | ALARM |
A 0.5% imbalance on 400k users is a five-sigma event. It means the arms are not comparable populations, so no analysis of the metric is valid. SRM is a gate before analysis, not a footnote after it.
Multiple comparisons
\(1 - 0.95^m\): 22.6% at 5 metrics, 40.1% at 10, 64.2% at 20. Declare one primary metric in advance; everything else is a guardrail or exploratory.
12. Recommendation Systems
EMA half-life
\(u_t = \alpha x_t + (1-\alpha)u_{t-1}\); half-life \(h = \ln(0.5)/\ln(1-\alpha)\). derived:
| α | Half-life (interactions) | Effective window 1/α | Weight on last 10 |
|---|---|---|---|
| 0.02 | 34.31 | 50.0 | 18.3% |
| 0.05 | 13.51 | 20.0 | 40.1% |
| 0.10 | 6.58 | 10.0 | 65.1% |
| 0.20 | 3.11 | 5.0 | 89.3% |
| 0.50 | 1.00 | 2.0 | 99.9% |
At α=0.5 the profile is "the last two things you clicked". At α=0.02 it will not notice a genuine interest change for a month. The mean profile is the α→0 limit and can never adapt. Choose α from a stated assumption about drift rate, then test that assumption — which is precisely what P09 exists to make possible.
Popularity concentration
Zipf exponent α, share of total engagement mass (derived):
| Zipf α | Top 1% of items | Top 10% |
|---|---|---|
| 0.5 | 9.4% | 31.1% |
| 0.8 | 30.0% | 57.1% |
| 1.0 | 53.0% | 76.5% |
| 1.2 | 75.1% | 90.3% |
At α=1.0, recommending only the top 1% of the catalogue captures 53% of engagement. A trivial bestseller list beats a mediocre personalised model on accuracy metrics, while covering 1% of the catalogue. This is why the popularity baseline is mandatory and why coverage must be reported next to NDCG, every time.
The retrieval ceiling
\[ \text{recall}_{\text{end-to-end}}@k \le \text{recall}_{\text{retrieval}}@K \]
Obvious once stated, routinely ignored. If retrieval recall@K is 0.90, no ranker can exceed 0.90 end to end. Teams spend quarters on ranking while the retrieval stage silently caps them. Your P02 index's recall is a hard ceiling on your P08 recommender, and measuring where that ceiling binds is one of the most satisfying cross-project measurements in the journey.
13. Floating Point
reported (IEEE 754), with the consequences that matter here.
| Format | Bits (s/e/m) | Decimal digits | Max | Min normal |
|---|---|---|---|---|
| fp64 | 1/11/52 | ~15.9 | 1.8e308 | 2.2e-308 |
| fp32 | 1/8/23 | ~7.2 | 3.4e38 | 1.2e-38 |
| bf16 | 1/8/7 | ~2.4 | 3.4e38 | 1.2e-38 |
| fp16 | 1/5/10 | ~3.3 | 65,504 | 6.1e-5 |
bf16 and fp16 are both 16 bits and they are not interchangeable. bf16 keeps fp32's 8-bit exponent and sacrifices mantissa; fp16 keeps 10 mantissa bits and shrinks the exponent to 5. Gradients routinely underflow fp16's 6.1e-5 minimum — which is exactly why fp16 training needs loss scaling and bf16 does not. bf16 won for training because range matters more than precision when your values span many orders of magnitude.
Machine epsilon for fp32 is \(2^{-23} = 1.19\times10^{-7}\). This sets your gradient check tolerance: 1e-5 relative is achievable, 1e-7 is not, and demanding 1e-9 in fp32 means chasing a bug that does not exist.
Floating-point addition is not associative. \((a+b)+c \ne a+(b+c)\) in general, so a
different summation order legitimately gives a different answer. Consequences you will
meet: your autodiff will not match PyTorch bit-for-bit and that is not necessarily a
bug (test in fp64 to distinguish); a fused kernel that reassociates changes results and
must document it; and numpy.sum uses pairwise summation while a naive loop does not, so
they disagree on large arrays and numpy is the more accurate one.
14. How These Numbers Lie
Four measurements on this page were wrong before they were right. Each failure is a general trap, and finding them is more instructive than the final numbers.
1. The prefetcher ate my latency measurement
First attempt: pointer chase with a fixed 64-byte stride. Reported 1.30 ns at 512 MB — claiming DRAM was as fast as L1.
Why: a constant stride of exactly one cache line is the single easiest pattern for a hardware prefetcher. Every "miss" was already in flight. Worse, with stride 8 pointers into a power-of-two array, \(\gcd\) made the cycle revisit only \(n/8\) distinct addresses, shrinking the true working set 8×.
Fix: Sattolo's algorithm — a single random cyclic permutation over one pointer per cache line. Random order defeats stride prefetchers; a single cycle guarantees every load depends on the previous one. Lesson: if a memory benchmark reports a flat line across the hierarchy, the prefetcher is doing your work.
2. The compiler deleted my benchmark
Second attempt at the same measurement returned 0.00 ns at every size. The chase
loop was eliminated: its only consumer was a local volatile that was then discarded.
Fix: a file-scope volatile sink assigned after the timed loop, plus
asm volatile("" ::: "memory"). Lesson: if a benchmark reports zero, it did not run.
Always print the raw elapsed time and iteration count, not just the derived per-op
figure — the derived figure hides the failure and the raw one exposes it instantly.
3. Three attempts at branch misprediction, three different wrong things
- Attempt 1:
if (data[i]) c++;— predictable vs random arrays. Both 0.19–0.20 ns, no difference. clang compiled the branch into a conditional add (cmov). There was no branch to mispredict. - Attempt 2: volatile counters in both arms to force a real branch. Predictable 2.02 ns, unpredictable 1.74 ns — the unpredictable case was faster. The volatile stores now dominated, and the predictable pattern hammered one address 1,024 times in a row, creating a store-forwarding dependency that the alternating case avoided. I was measuring store throughput.
- Attempt 3: abandoned.
I therefore report no measured branch-misprediction cost. The architectural figure is ~10–20 cycles (reported), which at 3.5 GHz is ~3–6 ns. Lesson: when a microbenchmark resists three careful attempts, the honest output is "not measured", not the number you expected to find. Publishing attempt 2's −0.28 ns as "misprediction is free" would have been worse than silence.
4. The page cache pretended to be a disk
Measured 14,976 MB/s sequential read and 1,063,490 IOPS at 4K random, with
fcntl(F_NOCACHE) set and incompressible random data, on the real internal SSD.
Both are impossible: 14.9 GB/s exceeds the 57.5 GB/s DRAM bandwidth by a suspicious
margin for a device read, and no consumer SSD does 1M IOPS at 4K. F_NOCACHE on APFS is
advisory; macOS has no O_DIRECT.
What survived: sequential write + fsync (~6,700–7,900 MB/s) and 4K write + fsync (~90 µs), because fsync must reach durable media to return. Those are real. The reads are page-cache numbers and are labelled as such.
Lesson: sanity-check every measurement against a physical bound you already know. The DRAM bandwidth number from §2 is what exposed the disk number in §3. Numbers that violate a bound you have independently measured are not surprising results; they are broken experiments.
The One-Page Table
Everything normalised to the L1 hit (0.91 ns) on the reference machine. Print this.
| Operation | Time | × L1 hit |
|---|---|---|
| L1 hit | 0.91 ns | 1 |
| Branch (predicted, in-loop) | ~0.2 ns | 0.2 |
| Relaxed atomic add | 2.01 ns | 2.2 |
| seq_cst atomic add | 3.97 ns | 4.4 |
| L2 hit | 5.94 ns | 6.5 |
| Uncontended mutex lock+unlock | 6.38 ns | 7.0 |
Python pass (loop floor) | 8.28 ns | 9 |
| Python dict lookup | 19.45 ns | 21 |
malloc + free | 18.71 ns | 21 |
| Python function call | 24.73 ns | 27 |
| DRAM (random) | 121.10 ns | 133 |
| Real syscall | 127.59 ns | 140 |
| numpy op dispatch (1-elem) | 254.56 ns | 280 |
| ANN distance, interpreted | ~899 ns | 988 |
| Context switch (best case) | ~1,530 ns (1,383–1,706) | ~1,680 |
| write(4K) + fsync | ~95,000 ns (90–105 µs) | ~104,000 |
| Context switch + 1 MB WS refill | ~2 ms (derived) | ~2.2M |
The three ratios to carry in your head
- L1 : L2 : DRAM = 1 : 6.5 : 133. Stable across decades and vendors.
- Syscall : context switch ≈ 1 : 12, and both are dwarfed by the cache pollution that follows. (Both endpoints vary run to run; the ratio is stable.)
- fsync : DRAM ≈ 780 : 1. Durability is the most expensive thing a program can ask for, by three orders of magnitude.
References
- Jeff Dean, Latency Numbers Every Programmer Should Know (via Peter Norvig, Teach Yourself Programming in Ten Years). The ancestor of this page. Most circulating copies are ~2012 vintage and the absolute values are stale; the ratios are not.
- Drepper, U. What Every Programmer Should Know About Memory. Red Hat, 2007. The definitive treatment of §1 and §2, including why pointer chasing is the correct latency probe.
- Williams, S., Waterman, A., Patterson, D. Roofline. CACM 52(4), 2009.
- Goldberg, D. What Every Computer Scientist Should Know About Floating-Point Arithmetic. ACM Computing Surveys 23(1), 1991. §13.
- Mytkowicz, T. et al. Producing Wrong Data Without Doing Anything Obviously Wrong! ASPLOS 2009. Measurement bias from link order and environment size — the formal version of §14.
- Dean, J., Barroso, L. A. The Tail at Scale. CACM 56(2), 2013. §10.
- Hoefler, T., Belli, R. Scientific Benchmarking of Parallel Computing Systems. SC 2015. Twelve rules; §14 is what happens when you break them.
- Little, J. D. C. A Proof for the Queuing Formula L = λW. Operations Research 9(3), 1961. The bytes-in-flight derivation in §2.
- Gregg, B. Systems Performance, 2nd ed. Pearson, 2020. The USE method and the correct way to measure disk without fooling yourself.
- Bloom, B. H. Space/time trade-offs in hash coding with allowable errors. CACM 13(7), 1970. §9.
- Sattolo, S. An algorithm to generate a random cyclic permutation. Information
Processing Letters 22(6), 1986. The single-cycle shuffle in
tools/machine-baseline.c; Fisher–Yates would produce several cycles and the walk would visit only a fraction of the working set. - Kohavi, R., Tang, D., Xu, Y. Trustworthy Online Controlled Experiments. Cambridge, 2020. §11.