Methodology¶
How every chart on this site is produced. Dashboard numbers load from docs/data/benchmarks.json, rebuilt by make results-json.
Two benchmarks¶
| Reference passing | Allocation | |
|---|---|---|
| Loop body | t = f(s) |
t = make_obj(i) |
| String | 12 MB, built once | 8-13 chars, new each iter |
| Iterations | 10,000,000 | 1,000,000 |
| Compilers | g++ -O3, rustc -C opt-level=3 | same |
12 MB avoids C++ small-string optimization so all languages hold string data on the heap.
Data pipeline¶
flowchart TD
subgraph wall["Wall-clock timing"]
HF["hyperfine --warmup 3"]
HF --> R1["results_bench.md"]
HF --> R2["results_bench_alloc.md"]
end
subgraph cache["Cache simulation"]
PR["make profile"]
PR --> CG["valgrind --tool=cachegrind"]
CG --> T1["results/cachegrind/cpp_bench.txt"]
CG --> T2["results/cachegrind/py_bench.txt"]
CG --> T3["... 4 more files"]
end
subgraph pyopt["Python variant suite"]
BP["make bench_python"]
BP --> PERF["python_optimized/PERF_REPORT.md"]
end
R1 --> JSON["make results-json"]
R2 --> JSON
T1 --> JSON
T3 --> JSON
PERF --> JSON
JSON --> DASH["charts on site"]
Build binaries¶
make all compiles cpp/bench, rust/bench, and alloc variants with -O3.
Wall-clock timing (hyperfine)¶
Statistical runs with warmup, exported to markdown:
hyperfine --warmup 3 --export-markdown results_bench.md \
'python3 python/bench.py' './cpp/bench' './rust/bench'
Same pattern for results_bench_alloc.md. These feed the timing charts (mean ± std bars).
Cache simulation (valgrind cachegrind)¶
Counts instructions, memory refs, and cache misses under simulation. Slower than native, but comparable across languages:
make profile # runs cachegrind on all 6 benchmarks
# stderr summaries land in results/cachegrind/*.txt
Parser in scripts/build_results_json.py extracts I refs, D refs, D1 misses, LL misses from each .txt file.
Python optimization suite¶
make bench_python runs hyperfine, cachegrind, and perf stat on each variant (baseline, inline, Cython, PyPy, and others). Output: python_optimized/PERF_REPORT.md.
Refresh dashboard data¶
make results-json merges all sources into docs/data/benchmarks.json. make docs-serve runs that step, then starts MkDocs.
hyperfine (wall clock)¶
Runs each command multiple times with warmup. Writes markdown tables consumed by scripts/build_results_json.py.
Charts: timing bars, gap summary, Python variant times.
valgrind cachegrind (simulated cache)¶
Not hardware counters. Valgrind replays the program and models L1/LL caches. Slow (Python ref-pass ~20 s) but apples-to-apples across languages.
make profile
# one file per benchmark, e.g.:
valgrind --tool=cachegrind --cachegrind-out-file=results/cachegrind/py_bench.out \
python3 python/bench.py 2>&1 | tee results/cachegrind/py_bench.txt
Parser reads the summary block at the end of each .txt:
| valgrind line | JSON field |
|---|---|
I refs |
instructions_m |
D refs |
data_refs_m |
D1 misses |
d1_misses |
LL misses |
ll_misses |
perf stat (hardware counters, Python suite only)¶
Used inside python_optimized/benchmark_python.py for cycles and IPC on each variant. Results merged into PERF_REPORT.md.
Regenerate JSON¶
Overhead split (estimated)¶
Hand-tagged breakdown from PERFORMANCE_ANALYSIS.md. We did not measure each component separately; the split is rough intuition. See the overhead charts on the reference passing and allocation pages.
Fairness caveats¶
Python includes interpreter call overhead. Baseline uses def f(x): return x. C++ and Rust inline to a near-zero-cost call. Inlining Python to t = s cuts time ~3× but leaves a ~50× gap vs C++.
C++ allocation uses SSO. Strings under ~15 chars may never call malloc. Python always allocates a PyObject on the heap. That is a real C++ advantage, not a pure allocator shootout.
Dead-code guards:
| lang | guard |
|---|---|
| C++ | asm volatile("" :: "r"(&t) : "memory") |
| Rust | std::hint::black_box(t) |
| Python | refcount side effects (no guard needed) |
Environment¶
AMD Ryzen 7 9700X, WSL2, CPython 3.10.12, g++ 11.4, rustc 1.93. Absolute milliseconds vary; ratios hold.