Add opt-in MI_SINGLE_THREADED specialization (idiomatic + safe) with analysis - #1
Add opt-in MI_SINGLE_THREADED specialization (idiomatic + safe) with analysis#1angelica-moreira wants to merge 5 commits into
Conversation
…p assert Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds bench/single-threaded with the full investigation behind MI_SINGLE_THREADED: - microbenchmarks (allocator-bound churn + reuse probe) - interleaved, pinned, perf-counter sweep + percentile/Mann-Whitney/bootstrap analysis - reference results (AMD EPYC 7742) and REPORT.md documenting both the delivered 3.5-4.6% speedup and the rejected ExGen-style single-free-list experiment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds an opt-in MI_SINGLE_THREADED build switch to specialize mimalloc’s free fast path for strictly single-threaded programs, plus a dedicated benchmark/analysis bundle under bench/single-threaded/ to reproduce and document the impact.
Changes:
- Introduces
MI_SINGLE_THREADED(off by default) and wires it through the public config header and CMake options. - Special-cases
mi_free_exto skip the per-free segment ownership check whenMI_SINGLE_THREADEDis enabled. - Adds reproducible microbench tooling, analysis scripts, and reference result datasets under
bench/single-threaded/.
Show a summary per file
| File | Description |
|---|---|
src/free.c |
Adds #if MI_SINGLE_THREADED fast-path specialization for is_local. |
include/mimalloc/types.h |
Documents and provides a default value for MI_SINGLE_THREADED. |
CMakeLists.txt |
Adds MI_SINGLE_THREADED CMake option and forwards it as a compile define. |
bench/single-threaded/scripts/run_micro.sh |
Runs pinned/interleaved perf-based microbench sweeps comparing prebuilt mimalloc variants. |
bench/single-threaded/scripts/build_variants.sh |
Builds baseline vs MI_SINGLE_THREADED variants into a chosen output directory. |
bench/single-threaded/scripts/analyze_micro.py |
Computes summary stats, MWU significance, and bootstrap CI from raw CSV samples. |
bench/single-threaded/results/real_benchmarks_n51.csv |
Reference dataset for real-benchmark sweep runs. |
bench/single-threaded/results/pgo_base_on_n21.csv |
Reference dataset for PGO “base vs on” runs. |
bench/single-threaded/results/microbench_base_pron_sota_n21.csv |
Reference dataset for microbench churn comparisons. |
bench/single-threaded/results/ipc_sh6bench_n11.csv |
Reference dataset for IPC-focused sh6bench runs. |
bench/single-threaded/REPORT.md |
Full write-up of rationale, methodology, and results (delivered + rejected experiment). |
bench/single-threaded/README.md |
Top-level reproduction guide and directory layout. |
bench/single-threaded/microbench/reuse.c |
Small helper program demonstrating reuse behavior. |
bench/single-threaded/microbench/churn.cpp |
Allocator-bound churn microbenchmark used by the scripts. |
Review details
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 14/14 changed files
- Comments generated: 2
- Review effort level: Low
| #if MI_SINGLE_THREADED | ||
| // single-threaded build: the block is always owned by the calling thread, so we | ||
| // skip the ownership test on the fast path. Verify the promise in debug mode. | ||
| mi_assert_internal(_mi_prim_thread_id() == mi_atomic_load_relaxed(&segment->thread_id)); | ||
| const bool is_local = true; |
| for i, a in enumerate(sys.argv): | ||
| if a == "--metric": metric = sys.argv[i+1] | ||
| if a == "--baseline": baseline = sys.argv[i+1] | ||
| d = defaultdict(lambda: defaultdict(lambda: defaultdict(list))) |
…ess fixes - free.c: use mi_assert (active at MI_DEBUG>=1) instead of mi_assert_internal (needs MI_DEBUG>1) so the single-threaded ownership safety net fires in a plain -DMI_DEBUG=ON build too. Verified: multithreaded misuse aborts at MI_DEBUG=1. - churn.cpp: reject mode/iters/window/size values that would divide by zero or write out of bounds. - run_micro.sh: error on leftover unmatched argv and on missing library paths. - analyze_micro.py: seed the bootstrap RNG (default 12345, --seed override) for deterministic confidence intervals. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Thanks @copilot — cross-checked both comments; both were valid and are now fixed in 4e0a0b6. 1. 2. Bootstrap CI non-deterministic — fixed. |
| sample(){ # <lib> <args> -> "cycles,instructions" | ||
| LD_PRELOAD="$1" $PIN perf stat -x, -e cycles,instructions $bin $2 2>&1 >/dev/null \ | ||
| | awk -F, '$3=="cycles"{c=$1} $3=="instructions"{i=$1} END{print c","i}' | ||
| } |
| The flag is a single-thread-only contract. With it on, calling mimalloc from | ||
| more than one thread is undefined. Unlike a bare branch removal, the debug build | ||
| keeps the ownership check as `mi_assert_internal`, so multithreaded misuse aborts | ||
| immediately (verified: `test-stress` trips it at `free.c` with SIGABRT) instead | ||
| of silently corrupting the heap. Zero cost in release. |
| ( cd "$d" && cmake "$here" -DCMAKE_BUILD_TYPE=Release "$@" >cmake.log 2>&1 \ | ||
| && make -j"$jobs" >make.log 2>&1 ) |
…PC, doc sync - build_variants.sh: use 'cmake --build' instead of 'make' (works with Ninja etc.) - analyze_micro.py: always compute IPC as instructions/cycles (was instructions/metric, which gave IPC=1.0 when --metric=instructions) - REPORT.md: sync the safety-net snippet/narrative to mi_assert (matches src/free.c) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Round 2 — thanks @copilot. Three of the four are fixed in 8b10c88; one is a false positive. ✅ ✅ ❌ and every |
Parse window/size as signed longs and require >0 before casting to size_t, so a negative argument no longer wraps to a huge size_t (avoiding a massive vector/ allocation attempt). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Summary
Opt-in, off-by-default
MI_SINGLE_THREADEDswitch that removes the per-freethread-ownership check (
_mi_prim_thread_id() == segment->thread_id) from thefree fast path. This is the idiomatic + safety-hardened version of the idea in
microsoft#1322, plus a full reproducible analysis under
bench/single-threaded/.What's in here
Code (
src/free.c,include/mimalloc/types.h,CMakeLists.txt)types.h(#if !defined(MI_SINGLE_THREADED) #define MI_SINGLE_THREADED 0)and a CMake option
-DMI_SINGLE_THREADED=ON, matching the existingMI_*switches.#if MI_SINGLE_THREADED(not an inline#if defined(...) && (...)).mi_assert_internal, so multithreaded misuse aborts immediately instead ofsilently corrupting the heap. Zero cost in release. The default build is
byte-identical to baseline.
Analysis (
bench/single-threaded/) — scripts, microbenchmarks, referencedata and
REPORT.md.Results (AMD EPYC 7742, pinned core, interleaved, perf counters)
The eliminated branch is perfectly predicted, so this only helps when the
allocator fast path is actually on the critical path. On allocator-bound
microbenchmarks (
bench/single-threaded/microbench/churn.cpp), cycles, n=21:On workloads where the allocator is not the bottleneck (e.g.
alloc-test,where 78% of branch-misses are in the benchmark's own RNG) the effect is ~0;
instruction count is not a proxy for speed on an out-of-order core. Full
discussion, codegen verification, and the rejected ExGen-style single-list
experiment (PR #2 /
single-threaded-sotabranch) are inbench/single-threaded/REPORT.md.Reproduce