Introduction
Rust lets you replace the default memory allocator with a custom one using the #[global_allocator] attribute. This is critical for embedded systems (which may not have a standard allocator), game engines (which need arena allocators for performance), and any application where allocation behavior must be controlled precisely.
Key Concepts
GlobalAlloctrait: The trait that custom allocators must implement:alloc,dealloc, and optionallyrealloc.#[global_allocator]: Attribute marking a static value as the global allocator for the program.Layout: Describes the size and alignment requirements of an allocation.- Arena allocator: An allocator that allocates from a fixed-size buffer and frees all memory at once.
Real World Context
Firefox uses jemalloc via a custom global allocator for better multithreaded performance. Embedded systems use bump allocators. Game engines use per-frame arena allocators that reset every frame. The mimalloc and jemalloc crates are popular drop-in replacements.
Deep Dive
Using a Third-Party Allocator
The simplest way to change the allocator is to use a crate like mimalloc:
rustuse mimalloc::MiMalloc; #[global_allocator] static GLOBAL: MiMalloc = MiMalloc; fn main() { // All heap allocations now use mimalloc let v = vec![1, 2, 3]; }
This single change can improve allocation-heavy programs by 10-30%.
Implementing GlobalAlloc
To write your own allocator, implement the GlobalAlloc trait:
rustuse std::alloc::{GlobalAlloc, Layout}; use std::sync::atomic::{AtomicUsize, Ordering}; struct CountingAllocator { alloc_count: AtomicUsize, dealloc_count: AtomicUsize, } unsafe impl GlobalAlloc for CountingAllocator { unsafe fn alloc(&self, layout: Layout) -> *mut u8 { self.alloc_count.fetch_add(1, Ordering::Relaxed); // Delegate to the system allocator unsafe { std::alloc::System.alloc(layout) } } unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { self.dealloc_count.fetch_add(1, Ordering::Relaxed); unsafe { std::alloc::System.dealloc(ptr, layout) } } } #[global_allocator] static ALLOCATOR: CountingAllocator = CountingAllocator { alloc_count: AtomicUsize::new(0), dealloc_count: AtomicUsize::new(0), };
This wraps the system allocator to count allocations — useful for detecting leaks.
Bump Allocator for Embedded
A bump allocator hands out memory from a fixed buffer:
rustuse std::alloc::{GlobalAlloc, Layout}; use std::sync::atomic::{AtomicUsize, Ordering}; struct BumpAllocator { heap: [u8; 4096], offset: AtomicUsize, } unsafe impl GlobalAlloc for BumpAllocator { unsafe fn alloc(&self, layout: Layout) -> *mut u8 { let align = layout.align(); let size = layout.size(); loop { let current = self.offset.load(Ordering::Relaxed); let aligned = (current + align - 1) & !(align - 1); let new_offset = aligned + size; if new_offset > self.heap.len() { return std::ptr::null_mut(); // Out of memory } if self.offset.compare_exchange_weak( current, new_offset, Ordering::AcqRel, Ordering::Relaxed ).is_ok() { return self.heap.as_ptr().add(aligned) as *mut u8; } } } unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) { // Bump allocators don't free individual allocations } }
Bump allocators are extremely fast (just increment a pointer) but cannot free individual allocations.
Common Pitfalls
- Returning unaligned pointers — The returned pointer must satisfy
layout.align(). Forgetting alignment causes UB. - Allocating inside the allocator — If your allocator implementation allocates (e.g., using
Vec), it causes infinite recursion. - Not handling zero-size allocations —
Layoutguarantees size > 0, but if you implementAllocator(nightly), you must handle zero-size types.
Best Practices
- Use established allocators for production —
mimalloc,jemalloc, andsnmallocare well-tested. - Wrap the system allocator for debugging — A counting/logging wrapper helps find allocation hotspots.
- Test with ASAN/MSAN — Address sanitizer and memory sanitizer catch allocator bugs that are invisible in normal testing.
Summary
#[global_allocator]replaces the program's default allocator.- Implement
GlobalAllocwithallocanddeallocmethods. - Third-party allocators like
mimallocare drop-in replacements. - Bump allocators are fast but cannot free individual allocations.
- Always respect
Layoutalignment requirements in custom allocators.
Code Examples
use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering};
/// An allocator that tracks total allocated bytes.
struct TrackingAllocator {
bytes_allocated: AtomicUsize,
}
unsafe impl GlobalAlloc for TrackingAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
self.bytes_allocated.fetch_add(layout.size(), Ordering::Relaxed);
unsafe { System.alloc(layout) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
self.bytes_allocated.fetch_sub(layout.size(), Ordering::Relaxed);
unsafe { System.dealloc(ptr, layout) }
}
}
#[global_allocator]
static ALLOC: TrackingAllocator = TrackingAllocator {
bytes_allocated: AtomicUsize::new(0),
};
fn main() {
let v: Vec<u8> = Vec::with_capacity(1024);
println!("Bytes allocated: {}", ALLOC.bytes_allocated.load(Ordering::Relaxed));
drop(v);
println!("After drop: {}", ALLOC.bytes_allocated.load(Ordering::Relaxed));
}