The global allocator
Every Box, Vec and String in the program reaches one allocator, and Rust calls it through a static value the whole crate shares. The GlobalAlloc trait describes it, and the attribute #[global_allocator] chooses the implementation.
This lesson wraps the system allocator in a counter, watches what a vector asks for while it grows, checks the alignment of a block, and ends with the compiler refusing an allocator that two threads could not share. The counted numbers are deterministic for the program shown, and they are the reason a Vec that starts empty allocates more than once.
A counting allocator
The trait has two required methods, alloc and dealloc, and a default realloc that calls them. A wrapper holds no data of its own, forwards to System and increments a static counter, which is enough to see every call the program makes.
The program builds a vector by pushing five values and prints how many allocations that took, the length and the capacity. It also prints whether at least one block was released, which happens when the vector moves its elements to a larger block.
use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering};
struct Counting;
static ALLOCATIONS: AtomicUsize = AtomicUsize::new(0);
static DEALLOCATIONS: AtomicUsize = AtomicUsize::new(0);
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
ALLOCATIONS.fetch_add(1, Ordering::Relaxed);
unsafe { System.alloc(layout) }
}
unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) {
DEALLOCATIONS.fetch_add(1, Ordering::Relaxed);
unsafe { System.dealloc(pointer, layout) }
}
}
#[global_allocator]
static ALLOCATOR: Counting = Counting;
fn main() {
let before = ALLOCATIONS.load(Ordering::Relaxed);
let mut values = Vec::new();
for value in 0..5 {
values.push(value);
}
let allocations = ALLOCATIONS.load(Ordering::Relaxed) - before;
println!("{} {} {}", allocations, values.len(), values.capacity());
println!("{}", DEALLOCATIONS.load(Ordering::Relaxed) >= 1);
}
rustc --edition 2024 counting.rs
./counting
2 5 8
true
Two calls for five elements: the fourth push allocates room for four and the fifth allocates room for eight, and the block of four is released through dealloc. The counts come from Ordering::Relaxed atomics, which is enough for a counter: the exact value matters only after a thread has been joined.
What Vec asks the allocator for
A capacity given up front is one allocation, whatever the length later becomes. The block is capacity * size_of::<T>() bytes with the alignment of the element, and the vector grows in place never: it always allocates a new block and copies.
The program reserves room for a hundred u64 values and prints the allocation count and the size of the block. One call and eight hundred bytes are the whole answer.
use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering};
struct Counting;
static ALLOCATIONS: AtomicUsize = AtomicUsize::new(0);
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
ALLOCATIONS.fetch_add(1, Ordering::Relaxed);
unsafe { System.alloc(layout) }
}
unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) {
unsafe { System.dealloc(pointer, layout) }
}
}
#[global_allocator]
static ALLOCATOR: Counting = Counting;
fn main() {
let before = ALLOCATIONS.load(Ordering::Relaxed);
let values: Vec<u64> = Vec::with_capacity(100);
let after = ALLOCATIONS.load(Ordering::Relaxed);
println!("{} {}", after - before, values.capacity() * std::mem::size_of::<u64>());
}
rustc --edition 2024 capacity.rs
./capacity
1 800
One call for eight hundred bytes is the whole cost of the reservation, and the pushes that follow do not touch the allocator until the hundredth. A shrink_to_fit at the end asks for one more call, which is why a container that has finished growing is better truncated once than grown in place. Vec::with_capacity is therefore the first thing to reach for in a loop that appends, and the lesson on Vec shows the same number from the outside.
Alignment and the System allocator
A layout can ask for more alignment than the type needs, and the allocator has to honour it. The system allocator returns a block whose address is a multiple of the requested alignment, which is what lets a type with a wide vector field sit in an array.
The program allocates eight bytes aligned to sixteen and prints whether the address is a multiple of sixteen. It frees the block with the layout it used.
use std::alloc::{alloc, dealloc, Layout};
fn main() {
let layout = Layout::from_size_align(8, 16).unwrap();
let pointer = unsafe { alloc(layout) };
println!("{}", pointer as usize % 16 == 0);
unsafe {
dealloc(pointer, layout);
}
}
rustc --edition 2024 aligned.rs
./aligned
true
The address itself is not printed because it changes between runs and between machines. The boolean is the property the layout asked for, and it holds for every call. A custom allocator is free to serve larger alignments from its own pool, which is what an arena does for a hot path.
The allocator is shared
The allocator is a static, and threads allocate at the same time, so the trait requires Sync. A counter inside the allocator must be an atomic for that reason; a Cell is rejected by the compiler before the program runs.
The program puts a Cell<usize> in the allocator and registers it. The compiler reports E0277 at the static and names the missing trait.
use std::alloc::{GlobalAlloc, Layout, System};
use std::cell::Cell;
struct Counting(Cell<usize>);
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
self.0.set(self.0.get() + 1);
unsafe { System.alloc(layout) }
}
unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) {
unsafe { System.dealloc(pointer, layout) }
}
}
#[global_allocator]
static ALLOCATOR: Counting = Counting(Cell::new(0));
fn main() {
let values = vec![1, 2, 3];
println!("{}", values.len());
}
rustc --edition 2024 sync_err.rs
error[E0277]: `Cell<usize>` cannot be shared between threads safely
--> sync_err.rs:18:19
|
18 | static ALLOCATOR: Counting = Counting(Cell::new(0));
| ^^^^^^^^ `Cell<usize>` cannot be shared between threads safely
|
= help: within `Counting`, the trait `Sync` is not implemented for `Cell<usize>`
= note: if you want to do aliasing and mutation between multiple threads, use `std::sync::RwLock` or `std::sync::atomic::AtomicUsize` instead
note: required because it appears within the type `Counting`
--> sync_err.rs:4:8
|
4 | struct Counting(Cell<usize>);
| ^^^^^^^^
= note: shared static variables must have a type that implements `Sync`
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0277`.
The note is the design in one sentence: every thread can reach the static, so every field it holds must tolerate that. An atomic is the smallest answer, and a lock is the general one.
A program has exactly one global allocator, and the attribute is allowed in a binary rather than in a library, because a library that set it would surprise every program that links it. A wrapper like the counting one is therefore a small crate of its own that the binary registers, and the alignment a layout asks for is part of the contract the wrapper has to forward unchanged.
Exercises
- Count realloc calls as well, by implementing the third method of the trait.
- Print the total bytes allocated by a loop that pushes ten thousand integers without a reservation, and again with one.
- Make the counting allocator print a warning when a single block is larger than one megabyte.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
Which trait does the global allocator implement?
Answer
GlobalAlloc
The trait has alloc and dealloc, and the attribute names the static that implements it.
-
How many allocations does a
Vecmake for five elements that start from empty?Answer
Two, one for four and one for eight
The vector doubles its capacity, so it allocates at four and again at eight.
-
What does a layout of size eight and alignment sixteen promise?
Answer
A block of eight bytes whose address is a multiple of sixteen
The size and the alignment are separate numbers, and the allocator honours both.
-
Why must the allocator static be
Sync?Answer
Every thread may allocate at the same time
A shared static is reachable from every thread, so its type has to allow that.
-
What does this program print?
fn main() { let values: Vec<u64> = Vec::with_capacity(10); println!("{} {}", values.len(), values.capacity()); }Answer
0 10
The reservation sets the capacity, and no element has been pushed yet.
-
What does this program print?
use std::alloc::Layout; fn main() { let layout = Layout::from_size_align(8, 16).unwrap(); println!("{} {}", layout.size(), layout.align()); }Answer
8 16
The two numbers are exactly the ones the layout was built from.
-
What does this program print?
fn main() { let mut values = Vec::new(); for value in 0..4 { values.push(value); } println!("{} {}", values.len(), values.capacity()); }Answer
4 4
The fourth push fits in the block of four that the first push allocated.
-
What does this program print?
use std::mem::size_of; fn main() { println!("{} {}", size_of::<Box<u64>>(), size_of::<u64>()); }Answer
8 8
The box is one pointer, and the value it points at is a single eight-byte number.
-
What is the first line of the compiler output for this program?
use std::alloc::{GlobalAlloc, Layout, System}; use std::cell::Cell; struct Counting(Cell<usize>); unsafe impl GlobalAlloc for Counting { unsafe fn alloc(&self, layout: Layout) -> *mut u8 { self.0.set(self.0.get() + 1); unsafe { System.alloc(layout) } } unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) { unsafe { System.dealloc(pointer, layout) } } } #[global_allocator] static ALLOCATOR: Counting = Counting(Cell::new(0)); fn main() { println!("{}", ALLOCATOR.0.get()); }Answer
error[E0277]:
Cell<usize>cannot be shared between threads safelyThe static is shared, and a Cell cannot be used from several threads.
-
What is the first line of the compiler output for this program?
use std::alloc::{GlobalAlloc, Layout, System}; struct Counting; unsafe impl GlobalAlloc for Counting { fn alloc(&self, layout: Layout) -> *mut u8 { unsafe { System.alloc(layout) } } unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) { unsafe { System.dealloc(pointer, layout) } } } #[global_allocator] static ALLOCATOR: Counting = Counting; fn main() { println!("{}", std::mem::size_of::<usize>()); }Answer
error[E0053]: method
allochas an incompatible type for traitThe trait declares alloc as unsafe, so an implementation without unsafe does not match it.
The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev