A reproducible C++ micro-benchmark suite for cache-line effects and memory allocators, built on Google Benchmark. Every benchmark ships with a short methodology writeup that follows the same discipline: hypothesis → measurement → conclusion, backed by repetitions and a variability measure — never a single run.
The reproducible method and the directional conclusions are the product; absolute numbers are host-dependent and always reported alongside the machine they were captured on.
- Rigorous, documented methodology. Each effect is stated as a hypothesis
(with the hardware mechanism behind it), measured under controlled conditions,
and concluded against the collected data — see
docs/methodology/. - Reproducibility is a first-class feature. CPU pinning, governor control, warmup/repetitions, a pinned toolchain, and self-describing result metadata are all wired into the harness and documented in the reproducibility guide.
- Real systems tooling. Scripted
perf stat/perf recordcapture and flame-graph rendering, all degrading gracefully whenperfpermissions or optional tooling are absent. - Portfolio-grade engineering. FetchContent-pinned dependencies, a shared utility library, unit tests (clean under ASan + UBSan), and CI + tag-triggered releases.
| Category | Benchmark | Effect demonstrated | Methodology |
|---|---|---|---|
cache_line |
False sharing | Packed atomics on a shared line vs. one-per-line padding under a thread sweep | false_sharing.md |
cache_line |
Alignment / straddle | An 8-byte load wholly inside a cache line vs. one that straddles the 64-byte boundary | alignment.md |
cache_line |
Access patterns | Sequential vs. strided vs. random access over a buffer larger than LLC | access_patterns.md |
allocators |
Pool vs. std | Fixed-block pool/arena allocator vs. std::pmr::new_delete_resource() |
pool_vs_std.md |
allocators |
std vs. optional external | std::malloc baseline vs. optional jemalloc / tcmalloc / mimalloc (feature-flagged, gracefully skipped when absent) |
— |
Two executables are produced, one per category:
bench_cache_line— false sharing, alignment, access patterns.bench_allocators— pool vs. std, and the optional external-allocator variants.
Required
- Linux (x86_64; developed and verified on Ubuntu / kernel 6.8)
- CMake ≥ 3.24
- Ninja
- A C++20 compiler (verified with GCC 13.3.0; clang 18.1.3 also works)
Google Benchmark and GoogleTest are fetched automatically via CMake
FetchContent — no manual install needed.
Optional
perf(linux-tools) for hardware-counter capture and flame graphs.- FlameGraph for rendering
perf recordoutput into an SVG. - jemalloc / tcmalloc / mimalloc development packages to enable the external allocator benchmarks (auto-detected; the suite builds and runs fine without them).
Build the pinned reference (release) preset and run the unit tests:
cmake --preset release
cmake --build --preset release -j
ctest --preset release # 19/19Run a benchmark directly:
# All cache-line benchmarks
./build/release/benchmarks/cache_line/bench_cache_line
# A single benchmark, machine-readable JSON, with repetitions for aggregates
./build/release/benchmarks/allocators/bench_allocators \
--benchmark_filter='BM_Alloc_Pool' \
--benchmark_repetitions=10 \
--benchmark_format=jsonUse the wrapper — it pins to a core with taskset, applies repetitions/warmup,
and writes both a results JSON and an environment-metadata sidecar. No root
required.
scripts/run_benchmark.sh build/release/benchmarks/cache_line/bench_cache_line \
--filter 'BM_FalseSharing_Packed|BM_FalseSharing_Padded' \
--core 2 --reps 10 --warmup 0 \
--out results/sample --name cache_line_false_sharingThis produces results/sample/cache_line_false_sharing.json (measurements +
mean/median/stddev/cv aggregates) and ….env.json (CPU model, compiler, flags,
governor, perf_event_paranoid, pinned core, reps/warmup).
For tighter numbers, optionally fix the CPU governor first (needs privilege; the script prints the exact command if unprivileged):
scripts/set_governor.sh performanceSee the reproducibility guide for the full
affinity / governor / turbo / warmup / perf_event_paranoid playbook.
Hardware counters (perf stat):
scripts/perf_stat.sh build/release/benchmarks/cache_line/bench_cache_line \
--filter BM_Access --core 2
# -> results/raw/<name>.perf-stat.txtRecord a profile (build the frame-pointer-preserving profiling preset first)
and render a flame graph:
cmake --preset profiling && cmake --build --preset profiling -j
scripts/perf_record.sh build/profiling/benchmarks/cache_line/bench_cache_line \
--filter BM_FalseSharing --core 2 --freq 997
export FLAMEGRAPH_DIR=~/FlameGraph # a clone of brendangregg/FlameGraph
scripts/flamegraph.sh results/raw/bench_cache_line.perf.data \
--out results/sample/cache_line_false_sharing.flame.svgThe perf scripts degrade gracefully: if perf is missing or
perf_event_paranoid is too high, they print an actionable message and exit
non-zero without corrupting artifacts.
The committed
results/sample/cache_line_false_sharing.flame.svgis a representative sample rendered from synthetic folded stacks (the reference host runsperf_event_paranoid=4with no root). Regenerate a live capture with the commands above on a perf-permitted host.
Captured on an Intel Core i7-6700 (4C/8T), GCC 13.3.0, -O2 -DNDEBUG, governor
powersave. Absolute numbers vary by machine; the directional effects are the
reproducible result.
| Benchmark | Comparison | Result |
|---|---|---|
| False sharing (8 threads) | packed vs. padded | ~9.9 M/s vs. ~91.4 M/s (~9×) |
| Alignment | aligned vs. straddling | ~3.32 G/s vs. ~2.49 G/s (~1.33×) |
| Access patterns | sequential vs. strided(16) vs. random | ~1.39 G/s vs. ~109 M/s vs. ~103 M/s (~13×) |
| Pool vs. std allocator | BM_Alloc_Pool vs. BM_Alloc_Std |
~98 M/s vs. ~32 M/s (~3×) |
Committed sample artifacts live under results/sample/
(false-sharing and pool-vs-std JSON + metadata sidecars, plus the sample flame
graph). Full context and per-thread breakdowns are in the
methodology writeups.
cpp-perf-benchmarks/
├── CMakeLists.txt # options, FetchContent, subdirectories
├── CMakePresets.json # release / profiling / debug presets
├── include/pcb/ # shared utility library headers (namespace pcb::)
├── src/ # environment, affinity, pool allocator
├── benchmarks/
│ ├── cache_line/ # false_sharing, alignment, access_patterns
│ └── allocators/ # pool_vs_std, std_vs_alt
├── tests/ # GoogleTest unit tests
├── scripts/ # run / perf / flamegraph / governor wrappers
├── cmake/ # FetchContent, allocator detection, warning flags
├── docs/
│ ├── requirements/SRS.md
│ ├── design/DESIGN.md
│ ├── methodology/ # per-benchmark hypothesis→measurement→conclusion
│ ├── reproducibility.md
│ ├── ci-cd/ # pipeline documentation
│ └── qa/QA-REPORT.md
└── results/sample/ # small committed reference artifacts
Two GitHub Actions workflows reuse the same presets as the local flow, so "green in CI" means the documented commands pass — details in the CI/CD pipeline doc:
ci.yml(push / PR tomain): areleasebuild +ctestjob and an AddressSanitizer + UndefinedBehaviorSanitizer job, with FetchContent caching.release.yml(on av*tag): builds, smoke-tests, and publishes Linux binary and source tarballs as a GitHub Release.
The project version is read from an optional top-level VERSION file (fallback
0.1.0); the VERSION file and the annotated v* tag are owned by the
release/versioning tooling.
- Add the source under
benchmarks/<category>/and register it in that category'sCMakeLists.txt(or add a new category subdirectory). - Copy
docs/methodology/TEMPLATE.mdtodocs/methodology/<category>/<name>.mdand fill every section in order: Hypothesis → Measurement → Results → Conclusion → Reproduce. - Capture a reproducible run with
scripts/run_benchmark.shand reference the numbers (and any committed sample artifact) from the writeup.
See the methodology index for the existing writeups as worked examples.
Released under the MIT License. © 2026 Vladislav Antonov.