Repository navigation
perf(build): revisit the mimalloc v2 pin when libmimalloc-sys vendors >=3.4.4 — upstream fixed the co-residency mechanism #448
Description
A watch item with a checkable trigger, not a task to start now. The v2 pin (#301) is still correct today; what changed is that upstream has fixed the underlying mechanism, so the pin now has an expiry condition instead of being permanent.
The trigger
libmimalloc-sys vendors mimalloc >= 3.4.4 on its v3 line. Check it at the source rather than by crate version, which does not name the C version:
# after `cargo update -p libmimalloc-sys --dry-run` tells you a newer release exists
grep "define MI_MALLOC_VERSION" \
~/.cargo/registry/src/*/libmimalloc-sys-*/c_src/mimalloc/v3/include/mimalloc.h
# 30302 = v3.3.2 (what 0.1.49 ships — the faulting build)
# >= 30404 = the fixed buildAt the time of filing: libmimalloc-sys 0.1.49 (published 2026-05-22) is the newest release and vendors v3 = 3.3.2, with the Apple fixed-slot path still in c_src/mimalloc/v3/include/mimalloc/prim.h. mimalloc 3.4.4 was released 2026-08-01. So the fix exists and has not reached Rust.
Why the pin can eventually go
microsoft/mimalloc#1327 is closed as completed. The mechanism is macOS-specific and narrower than our own notes suggest: v3 defaulted to fixed TLS slots 108/109, which Apple's libpthread reserves for the Swift runtime, so every co-resident v3 instance shared those two cells regardless of symbol visibility. The maintainer's closing note is that v3.4.4 uses pthreads TLS by default on macOS, which removes the collision.
That also explains the shape of the evidence gathered in #447: either side moving off the fixed slot clears the fault, which is why pyarrow 25.0.1 alone was enough with an unfixed v3 on our side.
Why it cannot go yet
- We do not choose the user's pyarrow, and 24.0.0-25.0.0 remain installable. Arrow bundles v3 across the board — 23.0.0 -> 3.1.5, 24.0.0 -> 3.2.7, main -> 3.3.1 (from the upstream thread) — so a fixed-slot v3 partner is the general case, not an exotic one.
- Our exposure note says "pyarrow 24.0.0-25.0.0", which is narrower than the mechanism. Any fixed-slot v3 co-resident qualifies.
- Shipping v3 from 0.1.49 today is shipping the faulting build and relying on everyone else in the process having been fixed.
What to re-run when the trigger fires
The harness from #447, with its controls validated (the positive control must reproduce the documented UnicodeDecodeError, or the run proves nothing):
- 2x2 corruption matrix — our pin (v2 / v3) x pyarrow (25.0.0 / 25.0.1), the test(python): intermittent heap corruption in an in-process native call fails the coverage gate ~1 run in 4 #294 minimal repro as probe, checking exit codes as well as output: the upstream report's crash is at teardown, which a probe reading stdout alone will miss.
- The arm that is still missing — a free-threaded CPython 3.14 (
3.14t) build. The 3.14 arm run in test: gate the mimalloc v2 pin off the manifests — and close out #297, which was #294 all along #447 was GIL-enabled, where CPython's vendored mimalloc is not the active allocator, so the CPython partner ourCargo.tomlcomment leans on has never actually been measured. That gap is worth closing whether or not the pin moves. - Perf — re-measure rather than quoting the figure recorded when the pin landed; take it from the benchmark suite, and remember the criterion drift on the dev machine (A/B/A, or say the instrument cannot resolve it).
Note for whoever picks this up
tests/test_allocator_pin_faithful.py (#447) asserts the feature on every crate that sets a #[global_allocator]. That is deliberate: unpinning has to be an edit to a test that explains why not, rather than a quiet dependency change. Update the gate as part of the decision, not around it.
Related: #294, #297, #301, #447, ags-wiki/concepts/arrow-c-ffi-allocator-ownership.md.
Activity
docs(wiki): the exposure window is a fact about partners, not the edg…
ci(bench): measure what the macOS mimalloc pin costs Linux and Windows (
niko86 commentedon Aug 20, 2026
Measured: the blanket pin costs Linux and Windows nothing
This issue notes that the pin is not platform-gated while the fault it guards is
macOS-specific — v3 defaulted to Apple's fixed TLS slots 108/109, which
libpthread reserves for the Swift runtime. So Linux and Windows carry a macOS
fix, and what that cost them had never been measured. It has now, on the
self-hosted runners, at the 25 MB
forge scalerung, 15 reps per arm.Two independent runs, both platforms
Each run builds
latthree ways — v2, v3, and no#[global_allocator]atall — and times them A/B/A. The system arm is a positive control: the perf
ledger records mimalloc-vs-system as a real win, so if it fails to separate, the
harness is not resolving allocators and nothing else in the run means anything.
Linux (
laterite-linux, glibc 2.31, rustc 1.98.0)Windows (
laterite-windows, Win11 VM, rustc 1.98.0)What it says
The control separated hard everywhere — mimalloc beats the system allocator
by ~36% on Linux and 53–63% on Windows. The instrument demonstrably resolves
allocator effects at this scale, which is what makes the null result below worth
anything.
Against that, v3 does not beat v2 on either platform. Every v2/v3 gap sits
inside its own run's noise floor, and the Windows deltas change sign between
runs (−1.7%, then +0.4%) — which is what a null result looks like from the
inside.
So narrowing the pin to
cfg(target_os = "macos")would buy Linux and Windowsnothing measurable. That inverts the case for doing it: the change would have
zero measured upside against three real costs — platform-divergent allocator
behaviour in a fault class that has already bitten this project (#294, #297,
#301), a
cfgin three manifests that today carry one flat line, and a morecomplicated
tests/test_allocator_pin_faithful.py.One honest caveat on the premise
"The fault isn't on Linux or Windows" is an inference from mechanism, not a
measurement — the fixed TLS slots are an Apple libpthread fact, and no Linux arm
was ever run (
arrow-c-ffi-allocator-ownership.mdhas nolinux/glibcmention). It is a strong inference, and it does not need testing: since
unpinning buys nothing, the safe choice and the fast choice are the same one.
What this does and does not close
It closes the cost half — that is no longer an open premise.
It does not close the issue. The trigger is unchanged and has not fired:
libmimalloc-sys0.1.49 still vendors v3 =30302(3.3.2), below the30404threshold, and
cargo update -p libmimalloc-sys --dry-runlocks 0 packages, sono newer release exists. And mac is unaffected by any of the above — pyarrow
25.0.1 being fixed does not release it, for the reasons
laterite-py/Cargo.tomlalready gives: we do not choose the user's pyarrow, and CPython 3.14 vendors its
own v3 regardless of pyarrow.
The re-run list's
3.14tarm also stands, and stays where it is — it onlybecomes decision-relevant at the moment someone is actually deciding to unpin.
The harness is retired in the same PR that lands this, per its reliquary row.
niko86 commentedon Aug 20, 2026
Closing: the pin is a decision now, not a watch item
Two things were open here. Both are answered, and neither leaves anything to
wait for.
1. What the blanket pin costs Linux and Windows — measured, and it is nothing
Recorded in the comment above: two runs on each self-hosted runner, 25 MB rung.
The system-allocator control separated by 35–36% (Linux) and 53–63% (Windows),
so the harness demonstrably resolved allocator effects — and against that, every
v2/v3 gap sat inside its own run's noise floor, with the Windows delta changing
sign between runs.
2. The CPython partner — answered by source, not by a corruption run
The re-run list asked for a free-threaded
3.14tarm, because "the CPythonpartner our
Cargo.tomlcomment leans on has never actually been measured."It has now been settled more strongly than that arm could have:
CPython vendors mimalloc v2.1.2, not v3.
Include/internal/mimalloc/mimalloc.hcarries
MI_MALLOC_VERSION 212on both the3.14branch andmain, and theshipped free-threaded build agrees. And v2 takes the contested macOS TLS slot
only under
MI_MALLOC_OVERRIDE, which CPython never defines — v3 made fixedslots 108/109 the macOS default for everyone, which is the whole mechanism.
So CPython is not on a different version of the same hazard; it is structurally
incapable of being in the collision. Three mimallocs share the process — ours,
pyarrow's (
default_memory_pool().backend_name→mimalloc), CPython's — andonly two were ever in the fault. Corrected in #480; the harness retires in #479.
Why this closes rather than staying open on the 3.4.4 trigger
The trigger was only ever a gate on a decision: upstream fixes v3 → we could
unpin → is unpinning worth it? That last step now has an answer, so the trigger
firing would not change what we do. Waiting on it keeps an issue open forever for
a question already settled.
And the one thing that would genuinely force a change is already automated.
If
libmimalloc-sysever drops the v2 line, the pin stops being a choice — andcargo catches that at resolution, before any build:
A hard build failure is a better watch than an issue whose manual check is "run
cargo update --dry-run, then grepMI_MALLOC_VERSIONout of a vendored Cheader" — which is exactly the check that never gets run.
The decision, recorded
mimalloc v2 is pinned on all three native artifacts, permanently, unless
libmimalloc-sysdrops the v2 feature. Its job is to keep us off the fixedslot so we cannot collide with pyarrow's copy, which is the only partner that was
ever in the fault. It is enforced by
tests/test_allocator_pin_faithful.py,which DISCOVERS the crates that set a
#[global_allocator]rather than listingthem, so a new surface cannot ship unpinned by omission.
Dropping our mimalloc entirely would sidestep co-residency altogether, and is
firmly the wrong trade: the control arm prices it at +36% (Linux) and +53–63%
(Windows).
chore(bench): retire the allocator harness — it has nothing left to m…
fix(docs): CPython is not a co-residency partner — it vendors mimallo…