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.