crucible.garethcooke.com
C++ performance engineering,
measured.
Each post is a focused optimisation problem — naive vs tuned implementations, real hardware measurements on a documented reference machine, and visualisations of system behaviour rather than algorithm steps.
At a glance
| # | Topic | Headline | Measured at | Finding |
|---|---|---|---|---|
| 01 | Branch prediction The CPU guesses which way an | 6.8× | N = 32 M, unsorted vs sorted | Unpredictable branches, not the work, dominate the loop — and branchless code recovers most of the gap without sorting. |
| 02 | False sharing Two threads write different variables that happen to share one 64-byte cache line, so the line ping-pongs between cores. | 13.6× | 8 threads across both CCXs, unpadded vs padded | Padding each accumulator onto its own cache line is the whole fix: layout beats algorithm. |
| 03 | SIMD One instruction operates on several values at once, using wide vector registers. | 10.3× | 1 M options, scalar libm vs AVX2+FMA | A polynomial erfc barely helps on its own, but it unlocks vectorisation that libm can't reach. |
| 04 | Lock-free queues Passing messages between threads with atomic operations instead of a lock that can put a thread to sleep. | 21.0× | p99.9 at 1 MHz offered load, mutex vs lock-free | Mutex latency is thread wake-ups; lock-free keeps the tail tight. |
| 05 | Allocators Where | No winner | freelist vs arena vs new/delete, two captures | Neither pool reproducibly beats the other; both beat new/delete on the tail, and the arena's tail stays flat under pressure. |
| 06 | Data layout (AoS vs SoA) Storing each record's fields together (array of structs) versus each field in its own array (struct of arrays). | 7.0× | N = 1 M, one hot field, AoS vs SoA | SoA wins only when few fields are hot and the data spills past L3. |
| 07 | Hash maps vs flat maps Choosing a key→value container: node-based tree, sorted vector, or open-addressing hash table. | No crossover | N = 8 to ~4 M, lookup and modify mixes | absl::flat_hash_map is fastest at every size; the expected small-N crossover never appears. |
| 08 | Sorting Comparison sorts can't beat n log n; radix sort sidesteps that by bucketing integer keys a byte at a time. | 4.8× | 67 M random u32 keys, std::sort vs radix | On narrow integer keys radix wins, and its cost doesn't depend on input order. |
| 09 | ARM NEON ARM's 128-bit SIMD instruction set, running the demo 03 kernel on a Raspberry Pi 5. | 4.8× | 1 M options, scalar poly vs NEON | The speedup is capped by vector width; there's no wider unit to reach for. |
| 10 | Core-to-core latency How long one core takes to see a cache line another core has just written. | 2.2× | median cross-CCX vs same-CCX round trip | Crossing the CCX boundary taxes every handoff; pin chatty threads within one CCX. |
- 01
Branch prediction
The CPU guesses which way an
ifgoes before it knows; a wrong guess throws away work already in flight.6.8×N = 32 M, unsorted vs sorted
Unpredictable branches, not the work, dominate the loop — and branchless code recovers most of the gap without sorting.
- 02
False sharing
Two threads write different variables that happen to share one 64-byte cache line, so the line ping-pongs between cores.
13.6×8 threads across both CCXs, unpadded vs padded
Padding each accumulator onto its own cache line is the whole fix: layout beats algorithm.
- 03
SIMD
One instruction operates on several values at once, using wide vector registers.
10.3×1 M options, scalar libm vs AVX2+FMA
A polynomial erfc barely helps on its own, but it unlocks vectorisation that libm can't reach.
- 04
Lock-free queues
Passing messages between threads with atomic operations instead of a lock that can put a thread to sleep.
21.0×p99.9 at 1 MHz offered load, mutex vs lock-free
Mutex latency is thread wake-ups; lock-free keeps the tail tight.
- 05
Allocators
Where
newgets its memory; custom pools avoid the general-purpose heap's locking and bookkeeping.No winnerfreelist vs arena vs new/delete, two captures
Neither pool reproducibly beats the other; both beat new/delete on the tail, and the arena's tail stays flat under pressure.
- 06
Data layout (AoS vs SoA)
Storing each record's fields together (array of structs) versus each field in its own array (struct of arrays).
7.0×N = 1 M, one hot field, AoS vs SoA
SoA wins only when few fields are hot and the data spills past L3.
- 07
Hash maps vs flat maps
Choosing a key→value container: node-based tree, sorted vector, or open-addressing hash table.
No crossoverN = 8 to ~4 M, lookup and modify mixes
absl::flat_hash_map is fastest at every size; the expected small-N crossover never appears.
- 08
Sorting
Comparison sorts can't beat n log n; radix sort sidesteps that by bucketing integer keys a byte at a time.
4.8×67 M random u32 keys, std::sort vs radix
On narrow integer keys radix wins, and its cost doesn't depend on input order.
- 09
ARM NEON
ARM's 128-bit SIMD instruction set, running the demo 03 kernel on a Raspberry Pi 5.
4.8×1 M options, scalar poly vs NEON
The speedup is capped by vector width; there's no wider unit to reach for.
- 10
Core-to-core latency
How long one core takes to see a cache line another core has just written.
2.2×median cross-CCX vs same-CCX round trip
Crossing the CCX boundary taxes every handoff; pin chatty threads within one CCX.
Posts
- 01
Sorted vs unsorted: a branch-prediction deep dive
Same code. Same data. Three variants. Why does one run 7× faster — and why does the branchless version close most of the gap without sorting anything?
2026-05-17 - 02
False sharing: a 13.6× throughput gap from 56 missing bytes
Same algorithm. Same fill stream. Same machine. The only variable is struct layout — and it dominates.
2026-05-16 - 03
Black-Scholes: same model, four implementations, ~10× spread
Polynomial approximations buy 11% on their own — but they're the gate to a 9× SIMD win that libm's erfc can't reach.
2026-05-14 - 04
Same queue API. Different tail by orders of magnitude.
End-to-end enqueue→dequeue, market-data thread to strategy thread. Lock-free SPSC vs mutex + condvar. At 1 MHz offered load, the median is already ~13× apart: lock-free ~130 ns, mutex ~1.7 µs. The tail diverges to ~20× at p99.9; past saturation, orders of magnitude and bistable.
2026-06-05 - 05
Allocators: cross-thread order pipeline
Cross-thread Order lifecycle benchmarked across three allocator strategies — malloc, freelist with return queue, and arena with batch handoff. The result that survives contact with the data isn't the one the design discussion would lead you to expect.
2026-06-05 - 06
AoS vs SoA: bandwidth amplification, not a crossover
Scanning a 128 B option-quote struct across the Zen 2 cache hierarchy. The AoS-vs-SoA gap is 7× when one field per element is hot and vanishes when every field is — and the SIMD win lands largest where memory bandwidth is smallest.
2026-06-05 - 07
No crossover: absl::flat_hash_map wins at every N and workload mix
Five map implementations swept from N=8 to N≈4M on a Zen 2 CCX. The 'small map → flat structure' folklore expected a crossover; the data found none. absl::flat_hash_map is fastest at every N, in both pure-lookup and modify-mixed workloads, and the sorted-vector primitives collapse under any modify load at non-trivial N.
2026-06-05 - 08
The comparison lower bound is a wall. Integer keys let you walk around it.
std::sort is the default everyone reaches for, and on fixed-width integer keys it's rarely the fastest. A look at why a radix sort sidesteps the Ω(n log n) wall, why input shape decides the comparison-sort race, and why a data-independent sort is the one you want when a worst-case input on a hot path is what you have to bound.
2026-06-05 - 09
NEON on ARM: vector width is not a given
The demo 3 Black-Scholes kernel on a Cortex-A76. NEON delivers ~4.3× over scalar at 16k, rising to ~4.8× at 1M — and stops there. Where Zen 2 had AVX2 to reach for, the Pi 5 doesn't. Vector width is a property of the silicon.
2026-06-02 - 10
Core-to-core latency: the seam inside a Zen 2 die
Two cores on the same chip, millimetres apart, talk in 72 ns or 160 ns depending on which side of an invisible line they sit on. Measuring the CCX boundary — and the placement effect that nearly turned into a phantom finding.
2026-07-28