malloc() is the allocator most programs get for free, and it is the wrong default for anything allocation-heavy and multithreaded. The problem is not memory capacity. It is metadata contention: every thread funnels through a shared heap, so allocator calls serialize, and throughput can get worse as you add cores.
Egbert Network’s survey traces how allocator design evolved to fix that, and turns the result into lookup tables.
The bottleneck
- The heap is a scalability bottleneck — calls through a shared allocator are serialized.
- Allocation-intensive programs can slow down as processor count rises.
- Don’t allocate per unit of work. The article’s example is per-packet allocation; kernel skbuff recycling and zero-copy capture (
PF_RING) are cited as roughly a 10% throughput win. - The line that frames everything: “Malloc (libc) is the worst memory allocation API to use.”
The fix: arenas
- The arena (a jemalloc term, around 2006) is a third memory pool, below the OS pool and the library pool.
- Each thread, core, or NUMA node allocates from its own region and touches shared state rarely.
- Original motivation: NUMA topologies, multi-speed memory banks, and per-core locality — not raw speed.
Frontend vs backend
- Frontend evolution: linked-list free space → size-class buckets → owner encoding → local allocation buffers → hazard pointers → arena pools → constant-time return to the OS.
- Backend evolution is about giving memory back and cutting RSS: Treiber-stack free lists, buddy algorithms, span-based tables, segment queues, distributed queues.
Workload → allocator, per the recommendation table
- Contended multithreaded → jemalloc (avoid dlmalloc, ptmalloc)
- Tail-latency sensitive → mimalloc
- NUMA / multi-socket, HPC → snmalloc
- Small-object-heavy RPC and web → tcmalloc
- Large or mixed sizes, fragmentation pressure → jemalloc
- False-sharing sensitive → Hoard; security-hardened → snmalloc
- Debugging and leak detection → libumem; legacy/ABI stability → glibc malloc
The reframing worth keeping is that the cost is atomic operations and cacheline bouncing, not bytes — which is why swapping the allocator is a real fix rather than a micro-optimization. Note the caveat: the ordering is asserted, not benchmarked in the piece. Treat the tables as a strong prior and measure the top two candidates against your actual access pattern.
The frontend/backend split also explains why “just use jemalloc” arguments stay vague — people conflate how memory is handed out with how it is given back, and only one of those usually matters for the problem at hand.