From 98e4c55255159f653786aa4fc7d918be9aa8988b Mon Sep 17 00:00:00 2001 From: Avi Avni Date: Tue, 4 Aug 2026 12:46:09 +0300 Subject: [PATCH] Topic 00: rewrite reading-drepper Steps 5-6, add a concept-to-profiler map MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reader feedback: Step 5 was unclear, Step 6's page-walk diagram was under-explained, and neither answered the general question — how do you identify these eight concepts in a program you did not write? Step 5 (dependent loads) now defines "dependent" at the instruction level, contrasts overlapping vs serialized misses on a timeline, and states the distinction it existed to teach: latency-bound and bandwidth-bound are opposites. A chase moves one 128 B line per ~104 ns (~1.2 GB/s) against the 24-57 GB/s a single core reaches on topic 12's scan. Both sides are priced from this repo's own measurements — hashmap at n=1e7 with independent probes at 9.3 ns vs cache_ladder at 128 MB with a dependent chase at 104 ns, an 11x gap that is overlap and nothing else. Each of cache_ladder's three construction choices is justified separately, including the carried cursor that fixed the ~25 ns "DRAM" reading. Step 6 (virtual memory) gains what its diagram assumed: why a radix tree at all (a flat table is 2^33 x 8 B = 64 GB per process), a bit-field diagram showing why 9-bit indices, each arrow labelled as a real load whose address comes from the entry just fetched, the three naming vocabularies, the Apple Silicon derivation (16 KB granule, 11-bit indices, three levels not four), and the four reasons it is not catastrophic. Closed against the measurement: cache_ladder's 87 -> 113 ns tail is +26 ns of page walk. New section "Finding these concepts in a real program" — three instruments in reach-for order, leading with the trap that a sampling profiler blames the waiting instruction; a decision funnel from IPC, branch-miss rate, achieved bandwidth and dTLB misses to one of five verdicts; and an eight-row table giving each concept its profile signature, its Linux counter, and the differential experiment that proves it. The differentials carry the weight because macOS has no perf. Also a fourth notes.md question (make the 1e7 probes dependent and report the number), two Done-when items, and a Tools block in the references. No new measurements — every figure was already in topic 0's notes.md and is now cross-referenced from the concept that explains it. Verified: mdbook build clean, the new flowchart renders to a real SVG under headless Chrome with no syntax error, and every relative link on the rendered page resolves. Co-Authored-By: Claude Opus 5 (1M context) --- SESSION-LOG.md | 12 + .../00-performance-toolbox/reading-drepper.md | 277 ++++++++++++++++-- 2 files changed, 259 insertions(+), 30 deletions(-) diff --git a/SESSION-LOG.md b/SESSION-LOG.md index 8b06707..9d7a6cc 100644 --- a/SESSION-LOG.md +++ b/SESSION-LOG.md @@ -9,6 +9,18 @@ Every performance figure quoted below is reproducible with `./verify.sh` (see [README.md](README.md)); timings depend on hardware, everything else is seeded. +## 2026-08-04 — topic 00 — reading-drepper: Steps 5–6 rewritten, plus a concept→profiler map + +**Reader feedback on [`reading-drepper.md`](topics/00-performance-toolbox/reading-drepper.md): Step 5 unclear, Step 6's diagram under-explained, and a general question — how do you identify these eight concepts in a program you did not write?** No new measurements; every number below already existed in [topic 0's notes.md](topics/00-performance-toolbox/notes.md) and is now cross-referenced from the concept that explains it. + +**Step 5 (dependent loads) rebuilt around one claim: the cost of a miss is not a property of the miss.** It opens with what "dependent" means at the instruction level (`chain[idx]`, `node->next->next`, a B-tree child pointer inside the parent you are still waiting for), then an ASCII timeline contrasting three overlapping misses (~105 ns total, ~35 ns apiece) with three serialized ones (~300 ns, 100 ns apiece), then the point people skip: **latency-bound and bandwidth-bound are opposites.** A chase moves one 128-byte line per ~104 ns ≈ **1.2 GB/s** against the **24–57 GB/s** a single core reaches on topic 12's streaming scan — slow while the bus idles, so no layout fix touches it. The payoff is a two-row table putting this repo's own numbers on both sides of the diagram: `lookup_shootout` `hashmap` at n=1e7, ~160 MB, 1024 **independent** probes → **9.3 ns**; `cache_ladder` at 128 MB, a **dependent** chase → **104 ns**. Same DRAM, same misses, **11×** apart on overlap alone — which is the real explanation for why the hash table looked suspiciously flat at ten million keys. Then the three construction choices of `cache_ladder` are each justified separately (the dependency lives in the *data* so no compiler or core can speculate around it; Sattolo's *cyclic* permutation both defeats the prefetcher and stops a short sub-cycle living in L1; the cursor carried across criterion iterations — with the original benchmark's ~25 ns "DRAM" reading cited as the failure it prevents), and the readout stated plainly: with no arithmetic between loads, `elapsed / steps` *is* one access's latency. + +**Step 6 (virtual memory) gained the five things the diagram assumed.** Why a tree at all — a flat table for a 47-bit space at 16 KB pages is 2³³ × 8 B = **64 GB per process**, so the radix tree is what buys sparsity, and depth is the price. A bit-field diagram of one x86-64 address (9/9/9/9 + 12-bit offset) showing *why* 9 bits — one table is one 4 KB page = 512 entries — with each arrow annotated as a real load whose address comes from the entry just fetched: **Step 5's chain, in silicon, before your access can issue.** A three-column naming table (PML4/PDPT/PD/PT = PGD/PUD/PMD/PTE = L0–L3) since all three appear in the sources. The Apple Silicon variant derived rather than asserted: a 16 KB granule means 2048-entry tables = 11-bit indices, and `14 + 11 + 11 + 11 = 47`, so the walk is **three levels, not four** — bigger pages buy a shorter walk *and* 4× the reach. And four reasons it isn't catastrophic: hardware walks it (a page *fault* is the kernel, microseconds, a different event), the tables are ordinary cacheable memory, page-walk caches hold partial translations, and huge pages turn an L2/PMD entry into a block descriptor covering 2 MB (32 MB at a 16 KB granule). Closed against the measurement: `cache_ladder`'s tail **87 → 113 ns** from 64 MB to 512 MB is **+26 ns** of walk once 32K pages overflow the TLB — not the +400 ns of a fully cold walk, not zero. "That +26 ns is this diagram, priced." + +**New section: "Finding these concepts in a real program"** — the general question, answered as a method. Three instruments in reach-for order, with the trap stated first: a sampling profiler attributes stall time to the *waiting* instruction, so memory-bound and compute-bound loops look identical, and this repo has the artifact to prove it (the `lookup_shootout` flamegraph's 21% SipHash / ~79% inlined probe loop, which no amount of staring could split). Counters name the wall (`perf stat` on Linux; Instruments → CPU Counters on macOS, which has no `perf`; a Linux VM for `perf c2c`). A **differential experiment** — change one thing, re-measure — is the only portable instrument and the one this repo leans on, because a counter says a number is high while a differential proves fixing it would help. A mermaid funnel routes from IPC + branch-miss rate + achieved GB/s + dTLB misses to one of five verdicts (compute / branch / bandwidth / translation / latency-bound), with the last split — same flamegraph, opposite fixes — as its point. Then an eight-row table: concept → signature in a profile → Linux counter → **the differential that proves it**. The sharp ones: pad a stride by one line to separate conflict from capacity; feed the same loop sorted vs shuffled indices to price the prefetcher; run *k* interleaved cursors and watch per-step time fall ~k× to prove spare MLP (which is what batched lookup APIs collect); `perf c2c` plus a 128-byte pad to confirm false sharing. Closes on two habits — always pair a counter with a differential, and compute useful-bytes ÷ bytes-moved by hand, since that ratio needs no profiler and decides row-vs-column in topic 12. + +Also: a fourth `notes.md` question (make `lookup_shootout`'s 1e7 probes dependent and report the new ns/probe — prove the MLP claim with the differential rather than by reasoning), two new `Done when` items (state the latency/bandwidth distinction and the one measurement that separates them; name counter *and* differential for all eight concepts), and a Tools block in the references. Verified: `mdbook build` clean, the new flowchart renders to a real SVG under headless Chrome with no syntax error, and every relative link on the rendered page resolves — including `experiments/benches/cache_ladder.rs`, which mdbook copies verbatim. + ## 2026-07-28 — repo-wide audit: verify.sh coverage, CI, and the conventions backfilled **Repo-wide review and fix pass — no new topics, but the measurement spine rebuilt.** Prompted by a full audit of all 44 packages, 230 reading guides, 45 crates and the book build. What the audit found and what was done: diff --git a/topics/00-performance-toolbox/reading-drepper.md b/topics/00-performance-toolbox/reading-drepper.md index b12b6b3..5efb981 100644 --- a/topics/00-performance-toolbox/reading-drepper.md +++ b/topics/00-performance-toolbox/reading-drepper.md @@ -3,9 +3,11 @@ Every latency table in topic 0 §2 is a compressed version of one 2007 paper — Drepper's "What Every Programmer Should Know About Memory". Before you open its 114 pages, this chapter builds the eight concepts the paper assumes, one -at a time — then hands you a section-by-section reading lens. The DDR2 -numbers are stale; the cache-organization math, the prefetching rules, and -the measurement methodology behind `cache_ladder` are forever. +at a time — then hands you a section-by-section reading lens, and finally a +table mapping each concept to the counter and the experiment that identify it +in someone else's program. The DDR2 numbers are stale; the cache-organization +math, the prefetching rules, and the measurement methodology behind +`cache_ladder` are forever. ## The problem in one sentence @@ -85,45 +87,174 @@ in the topic 0 latency table — same data, same cache, ~10× difference. ### Step 5 — dependent loads: the one latency you cannot hide -Out-of-order cores can overlap many *independent* misses (memory-level -parallelism: 10 misses in flight ≈ 10× cheaper per miss). But if load N+1's -*address* comes from load N's *result* — a linked list, a tree descent — no -overlap is possible. That's a **pointer chase**, and it measures raw latency: +A load is **dependent** when the core cannot compute its address until an +earlier load has come back. `chain[idx]` where `idx` was itself loaded from +memory; `node->next->next`; a B-tree descent, where the child pointer lives +inside the parent node you are still waiting for. + +Why that one word decides everything: an out-of-order core does not run one +load at a time. It keeps hundreds of instructions in flight and issues *every* +load whose address it already knows, so several misses sit in the memory +system simultaneously. This is **memory-level parallelism (MLP)**, and it +means the cost of a miss is not a property of the miss — it is a property of +how many other misses could keep it company. + +``` +independent loads — a[0], a[1], a[2]: all three addresses computable right now + + load A ├──────── ~100 ns ────────┤ + load B ├──────── ~100 ns ────────┤ three misses overlap in the + load C ├──────── ~100 ns ────────┤ memory system + └─► ~105 ns total ⇒ ~35 ns "per miss" + +dependent loads — B's address IS the value A returned + + load A ├──────── ~100 ns ────────┤ + load B ├──────── ~100 ns ────────┤ + load C ├─── ~100 ns ───┤ + └─► ~300 ns total ⇒ 100 ns each +``` + +Same cache, same DRAM, same number of misses — 3× apart here, and ~10× apart +in practice once the out-of-order window is full. Note what the diagram +implies: **latency and bandwidth are different questions.** A chase leaves the +memory bus nearly idle (one 128-byte line per ~104 ns ≈ 1.2 GB/s, against the +24–57 GB/s a single core reaches on a streaming scan in topic 12); it is slow +while doing almost nothing. Nothing you can do about the bus will help it. + +This repo measured both sides of the diagram on the same machine +([`notes.md`](notes.md)): + +| what | working set | ns per access | +|------|------------:|--------------:| +| `lookup_shootout` `hashmap` at n=1e7 — 1024 **independent** probes | ~160 MB | **9.3** | +| `cache_ladder` at 128 MB — a **dependent** chase | 128 MB | **104** | + +Both are random DRAM accesses that "should" cost ~100 ns. The 11× gap is +overlap and nothing else. That is why the hash table looked suspiciously flat +at ten million keys, and why a *single* isolated lookup in the capstone would +not enjoy the same number. + +The chase is how you measure the un-hidable case. Three properties of +[`cache_ladder`](experiments/benches/cache_ladder.rs) do the work: ```rust -// ring[i] holds the index of the next element to visit (a shuffled cycle). -// Because address N+1 is unknown until load N retires, ns/step == the raw -// latency of whatever level the working set lands in — L1, L2, SLC, DRAM. -fn chase(ring: &[usize], steps: usize) -> usize { - let mut i = 0; +fn chase(chain: &[usize], start: usize, steps: usize) -> usize { + let mut idx = start; for _ in 0..steps { - i = ring[i]; // serialized miss: nothing to prefetch + idx = chain[idx]; // the value loaded IS the next address } - i // return it so the loop isn't dead code + idx // returned so the loop isn't dead code } -// grow ring.len() from 16 KB to 512 MB and plot ns/step → the plateaus ``` -This is the measurement engine behind Drepper's famous Fig 3.4 *and* behind -this topic's `cache_ladder` experiment: plot ns/step against working-set -size, and the plateaus ARE the cache levels. +1. **The dependency is in the data, not the code.** `idx = chain[idx]` cannot + be reordered, hoisted, or speculated around by any compiler or any core — + the next address genuinely does not exist yet. One miss in flight, always. +2. **`chain` is a random cyclic permutation** (Sattolo's algorithm). Random + kills the prefetcher (Step 4) so nothing arrives early; *cyclic* — one cycle + through every slot — stops a short sub-cycle from quietly living in L1 and + flattering the big sizes. +3. **`idx` is carried across criterion iterations.** The first version of this + benchmark restarted at `idx = 0` each iteration, re-walked the same 65,536 + slots, and reported ~25 ns for "DRAM" — it had measured an ~8 MB hot path + that the benchmark itself created. The fix is in the source comment; + the confession is in `notes.md`. + +The readout is unusually direct: with no arithmetic between the loads, +`elapsed / steps` **is** the latency of one access at that working-set size. +Sweep the size from 16 KB to 512 MB and the plateaus are the cache levels — +this is Drepper's Fig 3.4, and it is the only number in this repo you can +compare to a datasheet without an argument. + +The database consequence is the whole curriculum: pointer-chasing layouts +(linked lists, naive trees, record-per-node graph stores) pay full latency per +hop, while layouts that expose addresses up front (arrays, matrices, +page-sized nodes, batched lookup APIs) convert latency into throughput. When a +later topic says a design "exposes memory-level parallelism", it means: the +addresses are knowable early enough to overlap the waiting. ### Step 6 — virtual memory: every address you use is fake -Your pointers are **virtual addresses**. Hardware translates each one to a -physical DRAM location via the **page table** — a map stored, awkwardly, -*in memory itself*, organized as a 4-level radix tree over 4 KB **pages** -(16 KB on Apple Silicon). Walking it costs up to 4 dependent loads: +Every pointer your program holds is a **virtual address**. The physical DRAM +location is decided by the OS — which gives each process its own address space, +maps pages lazily (your `Vec` allocation may have no physical memory behind it +until first touch), shares pages between processes, and backs some of them with +files. So *every* load needs a translation: virtual page → physical frame. The +map is the **page table**, and it lives, awkwardly, in memory itself. -``` -A TLB miss is pointer chasing in silicon — 4 dependent memory loads: +**Why a tree and not an array.** A flat lookup table would be one entry per +page: a 47-bit address space with 16 KB pages is 2³³ pages × 8 bytes = **64 GB +of table per process**. Unaffordable. So it is a **radix tree**: the virtual +address is chopped into fixed-width slices, each slice indexes one level, and +only the sub-tables that actually have mappings are ever allocated. An idle +process's page table is a few KB. You pay for sparsity with depth — and depth +here means *loads*. -CR3 ──► PGD entry ──► PUD entry ──► PMD entry ──► PTE ──► finally, your data - (load 1) (load 2) (load 3) (load 4) - each load can itself miss cache ⇒ worst case ~4 × DRAM latency - before the ACTUAL access even starts +**How the address is chopped** (x86-64, 4 KB pages — the paper's case): + +``` + 47 39 38 30 29 21 20 12 11 0 +┌────────────┬────────────┬────────────┬────────────┬─────────────┐ +│ 9 bits │ 9 bits │ 9 bits │ 9 bits │ 12 bits │ +│ L4 index │ L3 index │ L2 index │ L1 index │ offset │ +└─────┬──────┴─────┬──────┴─────┬──────┴─────┬──────┴──────┬──────┘ + │ │ │ │ └─ byte inside the page, + ▼ ▼ ▼ ▼ never translated + ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ + │ PGD │───►│ PUD │───►│ PMD │───►│ PTE │───► physical frame number + └──────┘ └──────┘ └──────┘ └──────┘ │ + ▲ load 1 load 2 load 3 load 4 ▼ + │ + offset ──► your data, at last + └── CR3: physical address of this process's top table (swapped on context switch) + +why 9 bits: one table is one 4 KB page = 4096 / 8 = 512 entries = 2⁹ +why dependent: each entry holds the *physical address of the next table*, so + load N+1's address is unknown until load N returns — Step 5's chain, in + silicon, running BEFORE your actual access can even issue ``` +The same tree, three vocabularies for the same four levels — you will meet all +three while reading: + +| level | x86 manuals | Linux source | ARMv8 | +|-------|-------------|--------------|-------| +| top | PML4 | PGD | L0 | +| ↓ | PDPT | PUD | L1 | +| ↓ | PD | PMD | L2 | +| leaf | PT | PTE | L3 | + +**Apple Silicon is shallower, because its pages are bigger.** With a 16 KB +granule a table holds 16384 / 8 = 2048 entries = **11 bits** of index, and the +offset takes 14 bits. So `14 + 11 + 11 + 11 = 47` — a 47-bit user address space +is covered in **three** levels, not four. Bigger pages buy a shorter walk *and* +4× the TLB reach (Step 7) from the same entry count. + +**Four things keep this from being catastrophic**, and knowing them is the +difference between fearing the diagram and predicting it: + +- **Hardware walks it, not the kernel.** The MMU's page-table walker does those + loads in silicon, costing nanoseconds. The kernel only gets involved when + there is no valid entry — a **page fault**, which is microseconds, a + thousand-fold different event. +- **The tables are ordinary cacheable memory.** The upper levels are touched by + every access in the region, so they normally sit in L1/L2; only the leaf level + is likely to be cold. +- **There are dedicated page-walk caches** (x86 paging-structure caches, ARM + walk caches) holding partial translations, so a walk often skips its first + levels entirely. +- **Huge pages truncate the walk.** An entry at the PMD/L2 level can be a + *block* descriptor pointing straight at 2 MB of contiguous physical memory + (32 MB with a 16 KB granule) instead of at another table — one fewer load, + and one TLB entry covering 512× more address space. + +So: worst case is ~3–4 dependent DRAM loads *added in front of* your access; +typical case is far less. The measurement is in `notes.md` and it lands where +this predicts — `cache_ladder`'s tail rises **87 → 113 ns** from 64 MB to +512 MB, an added ~26 ns per access once 32K pages overflow the TLB. Not the ++400 ns of a fully cold walk, not zero either. That +26 ns is this diagram, +priced. + ### Step 7 — the TLB: a cache for translations, with tiny reach Doing that 4-load walk per access would be absurd, so translations are @@ -170,6 +301,76 @@ details aged; the organization math, miss taxonomy, and measurement method didn't. Keep the Apple Silicon deltas in mind while reading: 128-byte lines (not 64), no inclusive L3 (shared SLC instead), much larger L1 (128–192 KB). +## Finding these concepts in a real program + +Steps 1–8 are visible in a microbenchmark you wrote on purpose. The harder +skill is spotting them in a program you did not write, where the pathology is +one loop among thousands. Three instruments, in the order you should reach for +them: + +1. **A sampling profiler** (`samply`, `cargo flamegraph`, Instruments → + Time Profiler) tells you *where* — which line owns the time. It cannot tell + you *why*, and here is the trap that matters for this whole chapter: a + sampling profiler attributes stall time to the instruction that is *waiting*. + A memory-bound loop and a compute-bound loop look identical — one hot + instruction — because "executing" and "blocked on DRAM" are the same sample. + This repo hit exactly that: the `lookup_shootout` flamegraph in `notes.md` + showed 21% in SipHash and ~79% in one inlined probe loop, and no amount of + staring at it could split "hashing" from "waiting". +2. **Hardware counters** tell you *which wall*. This is the only instrument + that distinguishes the eight concepts directly. On Linux: `perf stat`. On + macOS there is no `perf` — Instruments → **CPU Counters** template gives you + the events, and for real counter work run the same crate in a Linux VM or + container. +3. **A differential experiment** — change exactly one thing, re-measure — + is the only fully portable instrument, and the one this repo leans on. Each + row of the table below has one, because a counter tells you a number is high + while a differential proves the *causal* link. + +Start with the funnel: two counters (`instructions`, `cycles` → IPC) plus a +branch-miss rate narrow eight suspects to one or two. + +```mermaid +flowchart TD + A["IPC = instructions / cycles"] -->|"≥ ~2, and it scales
with more cores"| B["compute-bound
→ algorithm / SIMD (topic 17)"] + A -->|"IPC under ~1"| C{"branch misses above
~1% of branches?"} + C -->|yes| D["branch-bound
→ Step 3 of the README, topic 17"] + C -->|no| E{"achieved GB/s near
the machine's peak?"} + E -->|yes| F["bandwidth-bound
→ Steps 2, 4: line waste, layout"] + E -->|"no — bus mostly idle"| G{"dTLB misses
significant?"} + G -->|yes| H["translation-bound
→ Steps 6, 7: huge pages, smaller reach"] + G -->|no| I["latency-bound
→ Step 5: dependent loads, no MLP"] + style B fill:#1f6feb,color:#fff + style D fill:#8957e5,color:#fff + style F fill:#bf4b8a,color:#fff + style H fill:#bf4b8a,color:#fff + style I fill:#d29922,color:#000 +``` + +The last branch is the one people get wrong: **latency-bound and +bandwidth-bound are opposites.** Both look "memory-bound" in a flamegraph, and +they have opposite fixes — more bandwidth-efficient layouts do nothing for a +pointer chase, and more overlap does nothing for a saturated bus. + +| Concept | Signature in a profile | Counters (Linux `perf`) | Differential test that proves it | +|---|---|---|---| +| **1** Hierarchy at all | Low IPC, flat profile, time on loads | `cycles,instructions` | Shrink the dataset with the algorithm unchanged. Time/op drops sharply ⇒ you were paying the hierarchy, not the code. | +| **2** Cache-line waste | Hot loop touches one field of a wide struct | `cache-references,cache-misses`, plus achieved GB/s vs *useful* bytes | Split hot fields out (AoS→SoA) or shrink the struct. Faster with identical instruction count ⇒ you were paying for bytes you never read. | +| **3** Conflict misses | A cliff at a power-of-two size or stride, while the working set still "fits" | `L1-dcache-load-misses` high with a small working set | Pad the stride by one line (row stride 4096 → 4096+128). Faster ⇒ conflict, not capacity. Nothing else moves that. | +| **4** Prefetching | Sequential and random over the *same* data differ ~10× | (vendor-specific prefetch events; weak) | Feed the same loop a sorted vs shuffled index array. The gap *is* the prefetcher's contribution. | +| **5** Dependent loads | One load instruction owns the samples, IPC ≪ 1, **and achieved bandwidth is low** — slow while the bus idles | `cycles,instructions`; on x86 the stall-on-memory events | Run k independent chases interleaved with k cursors. Per-step time falls ~k× until it saturates ⇒ you were latency-bound with spare MLP. Batched/vectorized lookup APIs exist to collect that k×. | +| **6–7** TLB / page walks | A *second*, later cliff after the DRAM plateau has flattened | `dTLB-loads,dTLB-load-misses` (x86: `dtlb_load_misses.walk_completed`) | Enable huge pages (`MADV_HUGEPAGE` / THP) or drop the working set under TLB reach. On macOS: compare above vs below reach — that is `cache_ladder`'s last two rows, 87 → 113 ns. | +| **8** False sharing | Multi-thread scaling collapses; time sits in a *store*; per-thread work is unchanged | `perf c2c` — the purpose-built tool | Pad each thread's datum to its own line (128 B on M-series) and re-plot the scaling curve. Curve straightens ⇒ false sharing. | + +Two habits that make this reliable: + +- **Always pair a counter with a differential.** "Cache misses are high" is not + a diagnosis; databases miss cache constantly and are fine. The differential + answers the only question that matters — *would fixing it help?* +- **Compute the useful-bytes ratio by hand.** Bytes your algorithm needs ÷ + bytes the machine moved. It needs no profiler, catches Step 2 instantly, and + is the number that decides row-vs-column layouts in topic 12. + ## Questions to answer in notes.md when done 1. Why does `cache_ladder` show *gradual* transitions between plateaus rather than @@ -178,6 +379,11 @@ didn't. Keep the Apple Silicon deltas in mind while reading: 128-byte lines stop getting faster per element? Verify with a quick experiment. 3. How many memory accesses can a single TLB miss add on a 4-level page table, and why don't we see it in `cache_ladder`? (Hint: 16 KB pages, working set vs TLB reach.) +4. Take `lookup_shootout` at n=1e7 — 9.3 ns per probe over a ~160 MB table — and prove + with the Step 5 differential (not with reasoning) that it is latency-bound with spare + MLP rather than bandwidth-bound: make the probes *dependent* (each key derived from + the previous lookup's result) and report the new ns/probe. Which row of the + profiler table did you just walk down? ## Takeaway @@ -189,9 +395,11 @@ habit; the numbers you regenerate yourself on your own machine. - [ ] You can recite the latency ladder — L1, L2, L3, DRAM — within 2x, and say which numbers from 2007 have aged and which have not. - [ ] You can explain a conflict miss in terms of sets and ways, and construct a stride that causes one on purpose. -- [ ] You can say why a dependent-load chain is the one latency the prefetcher cannot hide, and why `cache_ladder` is built as a pointer chase for exactly that reason. +- [ ] You can say why a dependent-load chain is the one latency the prefetcher cannot hide, and why `cache_ladder` is built as a pointer chase for exactly that reason — including all three of its construction choices (data dependency, random *cyclic* permutation, cursor carried across iterations). +- [ ] You can state the difference between latency-bound and bandwidth-bound, name the one measurement that separates them, and say why a flamegraph never can. - [ ] You can compute a TLB's reach from entry count and page size, and explain why exceeding it looks like a second, later cliff. -- [ ] You wrote answers to all three questions in notes.md. +- [ ] Given a strange profile, you can name the counter *and* the differential experiment for each of the eight concepts, without re-reading the table. +- [ ] You wrote answers to all four questions in notes.md. ## References @@ -200,3 +408,12 @@ habit; the numbers you regenerate yourself on your own machine. 2007) — [PDF](https://people.freebsd.org/~lstewart/articles/cpumemory.pdf) (~114 pages — read §3–§4 properly, skim §6, skip the rest; the study guide's advice stands) + +**Tools referenced above** +- Brendan Gregg — [perf examples](https://www.brendangregg.com/perf.html) — + the counter-event cookbook behind the profiler table's middle column. +- [`perf c2c(1)`](https://man7.org/linux/man-pages/man1/perf-c2c.1.html) — + purpose-built false-sharing detection (Step 8); no macOS equivalent, so this + is one of the cases worth a Linux VM. +- Instruments → **CPU Counters** template — the macOS substitute for + `perf stat`; see topic 0 §4 for the full tool table on this machine.