← work
[systems]2025-10-01

XAlloc

Slab allocator in Rust — 2.7× faster than the system allocator at 8 threads. Thread-local caches keep the hot path lock-free; the system allocator's latency grows super-linearly under contention. XAlloc's stays flat.

Rustallocatorconcurrency

Overview

Most engineers reach for jemalloc or mimalloc when they need allocator performance. This project is about understanding why they're designed the way they are — by building one from scratch.

XAlloc is a slab allocator with thread-local caches, designed for workloads with many small allocations (32–512 bytes) across multiple threads. The core tradeoff: slower than the system allocator on a single thread, significantly faster under contention.

Benchmarks

| Threads | System Allocator | XAlloc | Speedup | |---------|-----------------|--------|---------| | 1 | 19 ns/alloc | 69 ns/alloc | 0.3x | | 2 | 54 ns/alloc | 92 ns/alloc | 0.6x | | 4 | 126 ns/alloc | 108 ns/alloc | 1.2x | | 8 | 346 ns/alloc | 128 ns/alloc | 2.7x |

The system allocator's latency grows super-linearly with threads due to lock contention. XAlloc's thread-local caches keep the hot path lock-free, so scaling is almost flat.

Architecture

Slab allocator — fixed-size chunks in 5 size classes (32, 64, 128, 256, 512 bytes):

  • O(1) alloc/dealloc via freelist manipulation
  • Reduced fragmentation vs general-purpose allocators
  • Each slab holds 64 chunks; new slabs allocated from OS in batches

Thread-local caches (16 chunks per thread per size class):

  • Fast path: lock-free cache access (~5ns)
  • Slow path: batch refill from global pool (16 chunks at once)
  • Result: 16x fewer lock acquisitions under load
#[global_allocator]
static ALLOCATOR: SlabAllocator = SlabAllocator::new();

fn main() {
    // All allocations automatically use XAlloc
    let v: Vec<u8> = Vec::with_capacity(128);
}

Key insight

The performance cliff at 4–8 threads comes from the system allocator's internal lock. XAlloc sidesteps this with thread-local caches — each thread has its own freelist per size class, and only falls back to the global pool when the cache is empty or full.

The batch refill strategy (16 chunks at once) amortizes the cost of crossing the lock boundary. A naive implementation that acquires the global lock per allocation would be slower.

Limitations

Not production-ready — only handles ≤512 bytes, uses spinlocks (not adaptive mutexes), and never returns memory to the OS. Use jemalloc in production. Build this to understand why.