Allocators in Rust Jemalloc Mimalloc and the System One
Swap the global allocator with one line. Compare the system allocator, jemalloc, mimalloc and snmalloc on fragmentation, thread scaling, memory return and portability, and measure before switching.

Every Vec, String and Box asks an allocator for memory. Rust lets you replace that allocator for the whole program with a single declaration, and for allocation-heavy or highly concurrent services the right one can improve speed and reduce memory. This is an optimization with real tradeoffs, so this post focuses on how to choose and measure. Crates: tikv-jemallocator 0.6, mimalloc 0.1, Rust 1.90 with edition 2024.
Switching
[dependencies]
mimalloc = "0.1"
# or
# tikv-jemallocator = "0.6"use mimalloc::MiMalloc;
#[global_allocator]
static GLOBAL: MiMalloc = MiMalloc;
fn main() {
let v: Vec<u64> = (0..1_000_000).collect();
println!("{}", v.len());
}// jemalloc variant
#[cfg(not(target_env = "msvc"))]
use tikv_jemallocator::Jemalloc;
#[cfg(not(target_env = "msvc"))]
#[global_allocator]
static GLOBAL: Jemalloc = Jemalloc;The #[global_allocator] static replaces the allocator for the binary, including allocations made by dependencies. jemalloc does not support the MSVC target, hence the cfg. Remember the allocator choice applies to a binary crate; a library should not set one.
The candidates
| Allocator | Strengths | Watch for |
|---|---|---|
| System (glibc malloc and equivalents) | Zero dependency, well-known behavior, honors tools like valgrind | Per-thread arenas can bloat memory; slow on musl |
jemalloc (tikv-jemallocator) | Strong under many threads, low fragmentation, profiling and statistics (jemalloc-ctl, heap profiles) | Larger binary, no MSVC support, tuning knobs to learn |
| mimalloc | Small, fast, portable, good defaults | Less introspection than jemalloc |
| snmalloc | Message-passing design that frees memory freed by another thread efficiently | Newer in the ecosystem; check support for your targets |
| bump or arena allocators | Extremely fast for batch workloads | Not a general global replacement; scoped use only |
Why it matters on musl
Static musl builds are popular for containers (static binaries with musl), and musl's allocator is simple and serializes through a lock. Multi-threaded programs that allocate heavily can slow down considerably. Adding mimalloc or jemalloc only for that target is a common fix:
#[cfg(target_env = "musl")]
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;Memory return and fragmentation
A service that spikes in memory use and then settles may keep a large resident size because the allocator retains freed memory to reuse it. Allocators differ in how eagerly they return memory to the OS and how they fragment under a long-lived mix of sizes. If your resident memory climbs and stays high, try another allocator and compare, or tune the current one (jemalloc has dirty_decay_ms and muzzy_decay_ms; glibc has MALLOC_ARENA_MAX to cap per-thread arenas, a common fix for containers with many cores). Distinguish allocator retention from a genuine leak by checking the live heap size with a heap profiler (measuring memory with heaptrack and DHAT).
Introspection with jemalloc
use tikv_jemalloc_ctl::{epoch, stats};
fn heap_bytes() -> (usize, usize) {
epoch::advance().unwrap();
let allocated = stats::allocated::read().unwrap();
let resident = stats::resident::read().unwrap();
(allocated, resident)
}allocated is bytes your code holds, resident is memory the process keeps from the OS. A growing gap is fragmentation or retention; a growing allocated is your program's data. Export both as metrics for a long-running service.
How to decide
- Establish a baseline with the default allocator: throughput, p99 latency and resident memory under a realistic load for a long run.
- Change one line to try mimalloc, then jemalloc. Rerun the identical test.
- Compare all three metrics, not only speed. A 5 percent speedup with 30 percent more memory is a different trade than one with less memory.
- Check the build constraints: targets (MSVC, musl, ARM), binary size, cross compilation.
- Keep the choice behind a Cargo feature so you can switch back.
Often the best optimization is fewer allocations rather than a faster allocator (reducing allocations), and for bursty request-scoped data an arena may remove allocator cost entirely.


