Skip to content
rusthacks.dev
Systems

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.

3 min read
Summarize in AI
Flat illustration for the post “Allocators in Rust Jemalloc Mimalloc and the System One”, composed around threads.

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

TOML
[dependencies]
mimalloc = "0.1"
# or
# tikv-jemallocator = "0.6"
rust
use mimalloc::MiMalloc;

#[global_allocator]
static GLOBAL: MiMalloc = MiMalloc;

fn main() {
    let v: Vec<u64> = (0..1_000_000).collect();
    println!("{}", v.len());
}
rust
// 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

AllocatorStrengthsWatch for
System (glibc malloc and equivalents)Zero dependency, well-known behavior, honors tools like valgrindPer-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
mimallocSmall, fast, portable, good defaultsLess introspection than jemalloc
snmallocMessage-passing design that frees memory freed by another thread efficientlyNewer in the ecosystem; check support for your targets
bump or arena allocatorsExtremely fast for batch workloadsNot 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:

rust
#[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

rust
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

  1. Establish a baseline with the default allocator: throughput, p99 latency and resident memory under a realistic load for a long run.
  2. Change one line to try mimalloc, then jemalloc. Rerun the identical test.
  3. 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.
  4. Check the build constraints: targets (MSVC, musl, ARM), binary size, cross compilation.
  5. 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.

Taggedallocator
Share

Keep reading

Get new posts by email

We send one email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.