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

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.

LabelMeaning
measuredProduced by running a named script on the reference machine. Reproducible. Will differ on yours — the shape will not
derivedComputed from measured quantities or from an identity. The arithmetic is shown so you can check it
reportedFrom a datasheet or paper. Not verified here. Treated with suspicion, and the suspicion is documented

And every entry answers four questions:

  1. What is the number?
  2. How was it obtained — with the derivation, not just the result.
  3. What assumption holds it up, and what makes it wrong?
  4. 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 setns/dependent loadLevelCycles @ 3.5 GHz
4 KB0.89L1d3.1
32 KB0.91L1d3.2
64 KB0.92L1d3.2
128 KB0.91L1d (still!)3.2
192 KB5.08L217.8
256 KB5.64L219.7
1 MB5.94L220.8
4 MB5.85L220.5
8 MB7.00L2 / SLC edge24.5
16 MB14.00transition49.0
32 MB69.08DRAM-ish241.8
64 MB97.85DRAM342.5
128 MB112.66DRAM394.3
512 MB121.10DRAM423.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

OperationGB/s
Sequential read57.5
Sequential write62.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:

HardwareDense peakBandwidthRidgeNote
H100 SXM989.4 TF/s3,350 GB/s295 F/Breported. 1979 TF/s is the 2:4 sparse figure — do not use it for a dense GEMM
A100 80GB312 TF/s2,039 GB/s153 F/Breported. 624 TF/s is sparse
Server CPU5.1 TF/s307 GB/s17 F/Breported, AVX-512 fp32, 8ch DDR5-4800
This machine~1.7 TF/s57.5 GB/s~29 F/Bderived 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.

OperationCostIn L1 hitsNote
Empty loop iteration0.31 ns0.34The measurement floor
getpid()1.23 ns1.4NOT a syscall — libc caches the pid
clock_gettime(MONOTONIC)18.00 ns20Also not a syscall — served from a shared page
close(-1) — real trap, fails immediately127.59 ns140A genuine user→kernel→user round trip
Pipe round trip, 2 processes3,272–3,864 ns~3,9004 syscalls + 2 context switches
⇒ implied context switch1,383–1,706 ns~1,680\((\text{rt} - 4\times128)/2\), derived
write(4K) + fsync90–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 why clock_gettime was 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 syscallsin 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.

OperationCostvs relaxed atomic
volatile long++0.33 ns
atomic_fetch_add, relaxed2.01 ns1.0×
atomic_fetch_add, seq_cst3.97 ns2.0×
pthread_mutex_lock + unlock6.38 ns3.2×
malloc(64) + free18.71 ns9.3×
malloc(64K) + free18.48 ns9.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,k1.91 GF/s3.14 GF/s
loop-reordered i,k,j27.33 GF/s27.27 GF/s
blocked, B=1610.0646.44
blocked, B=3215.1658.30 GF/s
blocked, B=6421.6438.29
blocked, B=12826.1533.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.

OperationnsIn L1 hits
pass (loop overhead floor)8.289
local variable read10.2011
global variable read10.4311
try/except (no raise)10.2411
int add12.7914
obj.x via __slots__12.8214
tuple unpack13.0614
obj.x (instance dict)13.1214
list[0]14.9516
list comprehension, per element16.2318
float add17.6919
dict['k']19.4521
isinstance()21.6824
function call24.7327
method call25.7728
string concat27.6530
numpy.float32 scalar add34.4338
next(generator)37.1641
f-string45.7450
raise + catch ValueError86.9296
numpy add, 1-element array254.56280
numpy add, 1M array (per element)0.25510.28
numpy.dot, 1M (per element)0.01360.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

PythonC equivalentRatio
dict lookup19.45 ns~1–5 ns hash+probe~4–20×
function call24.73 ns~1–2 ns~15–25×
float add17.69 ns~0.3 ns~60×
attribute access13.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:

ObjectBytes
float24
int28
tuple (empty)48
__slots__ instance, 2 attrs48
list (empty)56
dict (empty)64
set (empty)216
plain instance + __dict__, 2 attrs344

__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. , 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.

ComponentFLOPsScaling
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
TAttention GFFFN GFQuadratic shareScore matrix (fp32)
1280.651.212.7%0.8 MB
5123.224.8310.0%12.6 MB
10248.059.6618.2%50.3 MB
204822.5519.3330.8%201.3 MB
409670.8738.6547.1%805.3 MB
8192244.8177.3164.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:

BatchI (F/B)Step (ms)tok/sMFURegime
11.04.1792390.3%memory-bound
88.04.1791,9142.7%memory-bound
6464.04.17915,31421.7%memory-bound
256256.04.17961,25786.7%memory-bound
512512.07.24570,671100%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\):

dRC\(d_{10}/d_1\)
162.2201.2303
641.3561.0717
1281.2241.0465
5121.0971.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
uniform1.356
0.252.001.393 ← indistinguishable from uniform
0.151.201.608
0.100.801.992
0.050.403.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}} \]

DatasetAlgorithmicConstant factorPredictedMeasured
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:

efSearchuniform recall@10clustered recall@10uniform dists/qclustered dists/q
100.36050.4840397193
640.81600.83451,459321
1280.94800.90302,484501
2560.99300.96704,059840

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:

sK for k=10% of a 1M corpus
0.11000.01%
0.011,0000.10%
0.00110,0001.00%
0.0001100,00010.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\).

DataLevelsLeveled W / R / SSize-tiered W / R / S
1 GB221 / 3 / 1.103 / 20 / 2.11
8 GB331 / 4 / 1.104 / 30 / 2.11
64 GB331 / 4 / 1.104 / 30 / 2.11
512 GB441 / 5 / 1.105 / 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/keykFill ratioTheoryMeasuredRAM/100k keys
430.52750.146890.147090.05 MB
860.52810.021580.022040.10 MB
1070.50420.008190.008220.12 MB
16110.49730.000460.000470.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/keyfprDisk readsImprovement
none1.040.0
40.1475.88
100.008190.328122×
160.000460.0182,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\):

pSingle node2-of-3 quorumDowntime/year
0.0199.00%99.9702%3.65 d → 2.6 h
0.0595.00%99.2750%18.25 d → 15.9 h
0.1090.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:

NP(≥1 slow)
11.00%
109.56%
10063.40%
100099.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):

ScenarioCompletionvs idealWith backup tasks
no stragglers100.00 s1.00×
1% at 10×112.45 s1.12×108.64 s (1.09×)
5% at 10×149.87 s1.50×110.01 s (1.10×)
1% at 50×448.20 s4.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.051,570
0.0256,280
0.012525,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):

LooksFalse-positive rate
15.12%
28.58%
514.47%
1019.30%
2024.15%
5032.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,0000.000ok
201,000 / 199,00010.000ok (just)
202,000 / 198,00040.000ALARM

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.0234.3150.018.3%
0.0513.5120.040.1%
0.106.5810.065.1%
0.203.115.089.3%
0.501.002.099.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 itemsTop 10%
0.59.4%31.1%
0.830.0%57.1%
1.053.0%76.5%
1.275.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.

FormatBits (s/e/m)Decimal digitsMaxMin normal
fp641/11/52~15.91.8e3082.2e-308
fp321/8/23~7.23.4e381.2e-38
bf161/8/7~2.43.4e381.2e-38
fp161/5/10~3.365,5046.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.

OperationTime× L1 hit
L1 hit0.91 ns1
Branch (predicted, in-loop)~0.2 ns0.2
Relaxed atomic add2.01 ns2.2
seq_cst atomic add3.97 ns4.4
L2 hit5.94 ns6.5
Uncontended mutex lock+unlock6.38 ns7.0
Python pass (loop floor)8.28 ns9
Python dict lookup19.45 ns21
malloc + free18.71 ns21
Python function call24.73 ns27
DRAM (random)121.10 ns133
Real syscall127.59 ns140
numpy op dispatch (1-elem)254.56 ns280
ANN distance, interpreted~899 ns988
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

  1. L1 : L2 : DRAM = 1 : 6.5 : 133. Stable across decades and vendors.
  2. 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.)
  3. 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.