Skip to content

About

Reproducible micro/macro benchmarks with methodology

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

cpp-perf-benchmarks

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.

Why it's interesting

  • 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 record capture and flame-graph rendering, all degrading gracefully when perf permissions 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.

Benchmark catalogue

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.

Prerequisites

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 record output into an SVG.
  • jemalloc / tcmalloc / mimalloc development packages to enable the external allocator benchmarks (auto-detected; the suite builds and runs fine without them).

Quick start

Build the pinned reference (release) preset and run the unit tests:

cmake --preset release
cmake --build --preset release -j
ctest --preset release            # 19/19

Run 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=json

Reproducible runs (recommended)

Use 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_sharing

This 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 performance

See the reproducibility guide for the full affinity / governor / turbo / warmup / perf_event_paranoid playbook.

perf and flame graphs

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.txt

Record 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.svg

The 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.svg is a representative sample rendered from synthetic folded stacks (the reference host runs perf_event_paranoid=4 with no root). Regenerate a live capture with the commands above on a perf-permitted host.

Sample results (reference host, host-dependent)

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.

Project layout

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

CI and releases

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 to main): a release build + ctest job and an AddressSanitizer + UndefinedBehaviorSanitizer job, with FetchContent caching.
  • release.yml (on a v* 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.

Adding a new benchmark

  1. Add the source under benchmarks/<category>/ and register it in that category's CMakeLists.txt (or add a new category subdirectory).
  2. Copy docs/methodology/TEMPLATE.md to docs/methodology/<category>/<name>.md and fill every section in order: Hypothesis → Measurement → Results → Conclusion → Reproduce.
  3. Capture a reproducible run with scripts/run_benchmark.sh and reference the numbers (and any committed sample artifact) from the writeup.

See the methodology index for the existing writeups as worked examples.

License

Released under the MIT License. © 2026 Vladislav Antonov.

About

Reproducible micro/macro benchmarks with methodology

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages