Skip to content

Latest commit

 

History

51 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

bumblebench — the M2 Max, verified

Most of what you believe about the chip in your MacBook is folklore: numbers copied from an M1 reverse-engineering blog, review-site figures measured at a level sysctl doesn't distinguish, x86 intuitions that never ported, and vendor documentation that does not exist. This repository is the antidote: a fact ledger for the Apple M2 Max in which every claim is verified by an experiment built to kill it — and which you can re-run, in minutes, on your own machine.

git clone https://github.com/bjornpagen/bumblebench && cd bumblebench
bash scripts/verify-all.sh        # re-verifies the whole ledger, stamped with YOUR OS build

No dependencies. Every number traceable to source code and to the disassembly it compiled to — a number you can't connect to the machine code is a rumor.

The atlas

Every fact is also drawn. The atlas is a library of 39 measured plates — per-repetition scatter under a bold min-line, the fact's own tolerance band shaded onto the figure, regimes labeled, and a provenance stamp taken from the sweep data itself. Three to start:

The memory hierarchy, one dependent load at a time — and the SLC tier that isn't there:

the latency ladder

The branch predictor memorizes your benchmark: below ~8K outcomes a "random" pattern is nearly free, past ~16K every branch is a coin flip. Most microbenchmarks live on the left side of this cliff and don't know it:

the tage cliff

The out-of-order window, resource by resource: two DRAM misses stay overlapped until the filler between them exhausts the scheduler (~140), the load queue (~136), integer rename (~340), or the ROB (~590) — and nop fillers bind at none of them:

the window knees

Every plate re-renders from committed sweep data with scripts/atlas.sh --render-only, or from fresh measurements on your machine with scripts/atlas.sh.

Ten things this ledger can prove to you

  1. Your Mac has two cache-line sizes, and sysctl reports the wrong one for the L1D. The L1D manages 64-byte lines behind a memory system that moves 128 bytes; both published camps were right at different levels. (m2max.cache.l1d-line-64)
  2. The famous "~630-entry ROB" is ~590 — and it is almost never your bottleneck. The load queue (~136), the GPR rename window (~340), and the issue queue (~115–140) all bind first. Bonus: the classic nop-filler ROB microbenchmark measures nothing — nops retire-coalesce. (m2max.core.rob-size and siblings)
  3. There is no SLC latency tier for the CPU. Dependent loads see a monotonic ramp from L2 to DRAM — no 48 MB plateau — and naive "SLC latency" sweeps are confounded by a coincidence: the L2 TLB's translation reach is also 48 MB. (m2max.cache.slc-behavior)
  4. Page walks cost ~7 cycles here, not hundreds. The x86 horror stories don't port: a DTLB miss that hits the L2 TLB is ≈7 cycles, and even a full L2-TLB miss with resident tables is ≈35. (m2max.cache.tlb-miss-cost)
  5. "L1 is 3–4 cycles" is two facts wearing one number: 3 cycles simple addressing, 4 cycles scaled-index. Nobody had separated them. (m2max.cache.l1d-load-latency)
  6. A branch mispredict costs 15.5 cycles — except when it costs 28. The published "14–20" range is a staircase of in-flight state, and each stair is measurable. (m2max.core.pipeline-depth-mispredict)
  7. E-cores at background QoS run their full 2.4 GHz. The "~1 GHz background floor" belief is dead; E-cores are narrow, not slow — their L1 is the same cycle count as the P-core's. (m2max.ecore.alu-clock-ratio)
  8. An fsync can leave your P-core at 0.43 GHz — or leave you on an E-core entirely. Both halves of the previously published story (a 1.05–1.46 GHz floor, on one P core, always) were artifacts of the measuring probe's own demand. (m2max.clock.floor-e-migration)
  9. 128-bit NEON beats the "8-wide" scalar core 2.65× on min/max — not because vectors are wide, but because every flag-writing scalar op is confined to 3 of the 6 integer ALUs. Deep out-of-order does not substitute for SIMD on reductions. (m2max.core.flag-port-asymmetry)
  10. Your benchmark harness is lying to you more than the chip is. The predictor memorizes "random" branch data (4.7× swings on identical instructions), LLVM silently rewrites kernels under test, an unfenced timer stamp can misattribute a latency-bound span by −99.6% — and macOS's own clock APIs are slide-proof while raw counter reads are not. (m2max.predict.tage-memorizes-benchmarks, m2max.timer.stamp-slide-law)

Every one of those sentences carries a fact id. Look it up in FACTS.md — 66 entries across eleven domains — and each id resolves to a full dossier in facts/.

What a fact is

A fact here is not a benchmark result; it is a falsifiable claim with its number in the statement, and it carries, in order: the magnitude and its measurement protocol; the regime where it holds (most facts are regime tables — the single deepest lesson in this corpus is that a regime-free performance claim is usually wrong somewhere); the tolerance band outside which a re-run files a DRIFT; the falsifier — what observation would kill it; the antagonist — the binary whose --check mode attacks it, including a disassembly gate pinning the measured loop's machine code; an append-only verification log stamped with chip, macOS build, and toolchain; and graded provenance.

The canonical example is key-ahead prefetching (m2max.probe.key-ahead-prefetch), one mechanism with four verdicts: −21–27% on pressured hit streams, ≈0 on mixed streams, free on miss streams, +6.4% loss inside a loop whose memory parallelism is already saturated. Any document that gives you one number for it is wrong three-quarters of the time.

Statuses are a machine: VERIFIED (asserting check green, gated, stamped) · DRIFTED (tolerance violated — an open investigation, never a silent re-pin) · REFUTED (killed; the gravestone keeps the numbers) · IMPORTED (credible source, no local proof) · FOLKLORE (no credible source — prime prey). The only path upward is through an antagonist. Nothing is ever deleted.

The method

  • Antagonism. An experiment's job is refutation, not demonstration. Where a common wrong belief exists, the check asserts it fails as a negative control — a gate that cannot fail cannot protect. The suite has already caught its own authors: a polarity-inverted fact statement, an inverted CPU-cluster calibration, a runner that printed "all green" over a drifted run. The machinery wins arguments with the people who built it, which is the point.
  • The regime law. Every fact states the conditions under which it holds — cache tier, hit/miss mix, isolation vs in-situ, clock state. The antagonist's first move is the regime boundary, because that is where published claims go to die.
  • The disassembly is the measurand. Every antagonist's hot loop is pinned by objdump gates (scripts/asm-gates/). Compilers rewrite benchmarks: LLVM has auto-vectorized "scalar" baselines, reassociated "serial" dependency chains, and manufactured 10-cycle store-forward stalls out of innocent source in this very repo. Debug symbols stay on in release so the hot loops read with names.
  • Measurement is stamped and serialized. One machine-wide lock; spin, never sleep, before a timed region; min-of-repetitions; every run records chip, macOS build, rustc, and date, because the OS is part of the machine — the commpage clock behavior and the DVFS floor are OS facts a point release can move. REPORT.md diffs every run against the last; drift is a finding, on your machine too.

The full law — schema, status machine, discipline — is docs/00-charter.md. The style owes its spine to Casey Muratori: hand-rolled timing, no framework, data sized against the real hierarchy, machine code read and quoted in every write-up.

Running it

cargo run --release --bin tlb_geometry             # one experiment, exploratory tables
cargo run --release --bin tlb_geometry -- --check  # asserting mode: FACT lines, exit code
bash scripts/verify-all.sh                         # the whole ledger -> REPORT.md
objdump -d target/release/tlb_geometry | less      # read the hot loops yourself

The antagonists

Each binary has a companion write-up in docs/ — the question, the hypothesis, the measured tables, the relevant disassembly, and the verdict.

antagonist the question the answer
ooo_window how big are the OoO windows, really? ROB ~590 µops; load queue ~136; GPR rename ~340; the smallest window binds first, and it is never the ROB
tlb_geometry what does address translation cost? DTLB ≈256 entries, L2 TLB ≈3072 (48 MB reach); misses ≈7/≈35 cy — walks are cheap here
cache_latency the true latency ladder? L1 3/4 cy by addressing mode; L2 19 cy with ~6 MB clean reach; no SLC tier; DRAM ~122 ns direct (~101 memory + ~10 walk)
mispredict_depth what does a mispredict cost, alone? 15.5 cy isolated, linear in rate; up to ~28 with in-flight state
multicore_fabric how does the fabric scale? 57–59 GB/s per P thread, ~122 GB/s plateau (per-cluster cap ~60); atomics 76 ns same-cluster / 220–300 cross; E-cores at full 2.4 GHz under background QoS
floor_identity who runs at the fsync floor? the thread migrates to E-cores ~30% of deep-band time AND P-cores floor at 0.43–0.88 GHz — the published story was the probe's own equilibrium
mlp_batch_probes why didn't batching probes 37× help? the map was L2-resident: the OoO core overlaps probes at batch 1; ~33 effective miss lanes at DRAM
hash_probe_anatomy where do open-addressing inserts spend time? ⅓ stores, ⅓ walk, ⅓ hash latency exposed by exit-branch mispredicts; hash-ahead recovers 55–65% — on branchy probes only
sum_kernels does 2-lane NEON beat 6-wide scalar on sums? 19.6 vs 11.8 rows/ns at L1; exact sums 1.9× at every cache tier — adds/adcs live on a 3-port triad
neon_minmax why does the one NEON win win everywhere? cmp+csel on 3 ports vs cmhi/bsl on 4 vector pipes → 2.65× at every tier; flag µops stranded behind misses halve MLP
table_hoist_crossover where is the real hoist crossover? the "+48 ns/row" was std::array::from_fn refusing to inline; real crossover ≈ 4–8 items
prefetch_lead when does software prefetch land? when dependents clog the ~115-entry issue queue or a phased batch idles memory: 4× at DRAM, pure loss L2-resident
branchless_compaction is always-branchless right? 1.00 cy/item flat at every selectivity; branchy never wins on random data and is 9× worse at 50%
bounds_checks what do bounds checks cost in gathers? ~zero as instructions; 1.6–1.7× structural at L1 (the check's basic block breaks the load interleave), ≤5% elsewhere
identity_vs_gather the real bandwidth ceilings? ceilings are latency × parallelism walls: 128 GB/s zva fill, ~110 NT copy, 16 GB/s random-gather line traffic
l1d_line_and_aliasing 64 or 128-byte lines? does the aliasing cliff exist? both, at different levels; the feared set-aliasing cliff tops at 1.55× on real scans — the real 4–6× pathology is prefetch-tracker aliasing on power-of-two pitches
timer_costs what do the instruments cost? cntvct reads are 0.30 ns; isb fences cost more than the slide they prevent; unfenced stamps slide ≤ ~50 ns throughput-bound
call_overhead_leaf the per-row toll of call-per-element? the call is not the toll: SLP-merged accumulators manufacture a ~10-cycle un-renameable SIMD store→load round trip; bl/ret disables memory renaming
shadow_composition do probe optimizations compose? they are regime-dependent: window probing wins in cache and inverts 1.4–1.7× at DRAM; pairings must be measured jointly
flush_shape what does a batched phase's overhead cost? 9–30 ns per pass — 20× below the assumption an entire lever class was built on
dedup_floor what does runtime key-arity cost? 1.2–2.1× by regime; const-generic monomorphs get compiler fusion back for free
simd_probe can a NEON sweep beat the scalar probe walk? 3.5 ns flat in isolation — and it still loses in situ, because it touches key lines on misses the tag-gated walk never loads
fsync_dvfs what does an fsync leave behind? a demand-recovered clock floor, an E-core wake lottery after sleeps, and a probe-equilibrium trap that took a second antagonist to see through
residency_budget what does co-residency displace? hit streams pay first (+21% at 2–4 MB of streaming co-tenant); key-ahead prfm recovers 21–29% — on hit streams only
probe_residual why do "resident" structures miss in situ? residency is a property of phase interleaving, not footprint; full prefetch coverage buys the displaced cost back
attribution_error how wrong can per-op attribution be? a mis-counted denominator reproduces a 3× error class; latency-bound spans misattribute up to −99.6% with raw stamps

(Plus follow-up instruments — hash_probe_drift, hash_probe_followup, shadow_composition_followup, dedup_floor_followup — built to chase specific anomalies; see their parent write-ups.)

The machine

All facts are pinned on one machine and stamped per run: Apple M2 Max (Mac14,5), macOS (build recorded in every report), rustc per rust-toolchain. That specificity is the point — "Apple Silicon" generalizations are exactly the folklore this ledger exists to retire. If you run it on an M1, M3, or M4, the drifts you see are not noise: they are the cross-generation deltas nobody has published. Open an issue with your REPORT.md.

License

0BSD — use anything, no attribution required.

About

The living, antagonistic fact ledger of the Apple M2 Max — every microarchitectural claim verified by an experiment that tried to kill it

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages