Always Bump Downwards: The Arena Allocator rustc Uses for Everything
rustc's arenas allocate downwards from the end of a chunk, tell LLVM about the alignment with assert_unchecked, and grow from 4 KiB to 2 MiB chunks.
TL;DR: rustc allocates almost everything — types, HIR, MIR, spans — from bump allocators. The trick is that the arena allocates downwards from the end of a chunk, and tells LLVM that the pointer is aligned with
hint::assert_unchecked, which lets the compiler delete the alignment arithmetic entirely.
The arena
// compiler/rustc_arena/src/lib.rs:344-362
/// An arena that can hold objects of multiple different types that impl `Copy`
/// and/or satisfy `!mem::needs_drop`.
pub struct DroplessArena {
/// A pointer to the start of the free space.
start: Cell<*mut u8>,
/// A pointer to the end of free space.
///
/// The allocation proceeds downwards from the end of the chunk towards the
/// start. (This is slightly simpler and faster than allocating upwards,
/// see <https://fitzgeraldnick.com/2019/11/01/always-bump-downwards.html>.)
/// When this pointer crosses the start pointer, a new chunk is allocated.
///
/// This is kept aligned to DROPLESS_ALIGNMENT.
end: Cell<*mut u8>,
/// A vector of arena chunks.
chunks: RefCell<Vec<ArenaChunk>>,
}The allocation proceeds downwards. The comment links to a blog post arguing why, but the short version is that downward allocation is marginally simpler: the alignment fix-up is a subtraction and a mask on a pointer that moves down, and the “have we run out?” check is a single unsigned comparison against the chunk’s start.
The allocation loop
// compiler/rustc_arena/src/lib.rs:419-453
#[inline]
pub fn alloc_raw(&self, layout: Layout) -> *mut u8 {
assert!(layout.size() != 0);
// This loop executes once or twice: if allocation fails the first
// time, the `grow` ensures it will succeed the second time.
loop {
let start = self.start.get().addr();
let old_end = self.end.get();
let end = old_end.addr();
// Align allocated bytes so that `self.end` stays aligned to
// DROPLESS_ALIGNMENT.
let bytes = align_up(layout.size(), DROPLESS_ALIGNMENT);
// Tell LLVM that `end` is aligned to DROPLESS_ALIGNMENT.
unsafe { hint::assert_unchecked(end == align_down(end, DROPLESS_ALIGNMENT)) };
if let Some(sub) = end.checked_sub(bytes) {
let new_end = align_down(sub, layout.align());
if start <= new_end {
let new_end = old_end.with_addr(new_end);
// `new_end` is aligned to DROPLESS_ALIGNMENT as `align_down`
// preserves alignment as both `end` and `bytes` are already
// aligned to DROPLESS_ALIGNMENT.
self.end.set(new_end);
return new_end;
}
}
// No free space left. Allocate a new chunk to satisfy the request.
// On failure the grow will panic or abort.
self.grow(layout);
}
}The hint::assert_unchecked is the interesting line. It is a
compile-time-only assertion that LLVM treats as an assume: given
end % 8 == 0 and bytes % 8 == 0, LLVM knows end - bytes is already
8-aligned, so align_down(sub, layout.align()) reduces to a plain mask
and — crucially — the whole “align the size up to 8” step collapses away.
Without the assume, LLVM must conservatively keep the full align_up /
align_down arithmetic in the hot path.
The invariant really holds: grow aligns the chunk end down to
DROPLESS_ALIGNMENT, and every allocation subtracts
align_up(size, DROPLESS_ALIGNMENT) — a multiple of 8.
Chunk growth
// compiler/rustc_arena/src/lib.rs:96-102
// The arenas start with PAGE-sized chunks, and then each new chunk is twice as
// big as its predecessor, up until we reach HUGE_PAGE-sized chunks, whereupon
// we stop growing. This scales well, from arenas that are barely used up to
// arenas that are used for 100s of MiBs. Note also that the chosen sizes match
// the usual sizes of pages and huge pages on Linux.
const PAGE: usize = 4096;
const HUGE_PAGE: usize = 2 * 1024 * 1024;and:
// compiler/rustc_arena/src/lib.rs:380-417
#[inline(never)]
#[cold]
fn grow(&self, layout: Layout) {
// Add some padding so we can align `self.end` while
// still fitting in a `layout` allocation.
let additional = layout.size() + cmp::max(DROPLESS_ALIGNMENT, layout.align()) - 1;
unsafe {
let mut chunks = self.chunks.borrow_mut();
let mut new_cap;
if let Some(last_chunk) = chunks.last_mut() {
// There is no need to update `last_chunk.entries` because that
// field isn't used by `DroplessArena`.
// If the previous chunk's len is less than HUGE_PAGE
// bytes, then this chunk will be least double the previous
// chunk's size.
new_cap = last_chunk.storage.len().min(HUGE_PAGE / 2);
new_cap *= 2;
} else {
new_cap = PAGE;
}
// Also ensure that this chunk can fit `additional`.
new_cap = cmp::max(additional, new_cap);
let chunk = chunks.push_mut(ArenaChunk::new(align_up(new_cap, PAGE)));
self.start.set(chunk.start());
// Align the end to DROPLESS_ALIGNMENT.
let end = align_down(chunk.end().addr(), DROPLESS_ALIGNMENT);
...Geometric growth: 4 KiB, 8 KiB, 16 KiB, … capped at 2 MiB, which is
Linux’s HUGE_PAGE. The sizes deliberately match the OS’s page sizes so
the allocator can hand back huge-page-aligned memory.
Note grow is #[cold] #[inline(never)] — the chunk allocation path
never bloats the hot inline path.
Two arenas, one macro
// compiler/rustc_arena/src/lib.rs:597-627
/// Declares an `Arena` that can allocate values of a variety of `Copy`, `needs_drop` and
/// `!needs_drop` types.
///
/// The declared arena actually contains a single [`DroplessArena`], plus a separate
/// [`TypedArena`] for each of the types listed in the body of the macro invocation.
///
/// Any type that is `Copy` can be allocated in the arena without needing to be listed
/// explicitly. Those values will be stored in the [`DroplessArena`].
///
/// Types that are `!Copy` can only be allocated if they are listed in the macro invocation.
/// For types that are `!Copy + needs_drop`, values will be stored in the corresponding
/// [`TypedArena`] and will be dropped when the arena is dropped.
///
/// As an optimization, types that are `!Copy + !needs_drop` will actually be stored in the
/// `DroplessArena`, and the corresponding [`TypedArena`] will remain empty. This makes
/// better use of the dropless arena's storage blocks, while the overhead of having a few
/// unused typed-arenas is negligible.
#[rustc_macro_transparency = "semiopaque"]
pub macro declare_arena!(
// Each of these entries becomes a `$name: TypedArena<$ty>` field in the arena.
// This allows values of non-copy type $ty to be allocated in the arena.
// The field names must be distinct, but have no further significance.
$(
$name:ident: $ty:ty,
)*
) {A declarative macro (macro-rules 2.0) generates the arena type at the use site, so it is fully specialised and inlinable — no vtable, no dynamic dispatch.
And the routing:
// compiler/rustc_arena/src/lib.rs:639-679
// Any type that impls `Copy` can be arena-allocated in the `DroplessArena`.
impl<'tcx, T: Copy> ArenaAllocitable<'tcx, rustc_arena::IsCopy> for T {
#[inline]
#[allow(clippy::mut_from_ref)]
fn allocate_on(self, arena: &'tcx Arena<'tcx>) -> &'tcx mut Self {
arena.dropless.alloc(self)
}
...
}
$(
impl<'tcx> ArenaAllocitable<'tcx, rustc_arena::IsNotCopy> for $ty {
#[inline]
fn allocate_on(self, arena: &'tcx Arena<'tcx>) -> &'tcx mut Self {
if !::std::mem::needs_drop::<Self>() {
arena.dropless.alloc(self)
} else {
arena.$name.alloc(self)
}
}DroplessArena never runs destructors, so it is only valid for
!needs_drop types. A droppable type goes to its own TypedArena<T>,
which knows how many elements were initialised and drops them at teardown.
But a !Copy + !needs_drop type — Option<NonZeroU32>, say — still goes
into the dropless arena, because needs_drop is a compile-time constant
that folds away. The comment says the trade explicitly: better use of the
dropless storage, at the cost of a few unused typed arenas.
Why arenas at all?
Because rustc allocates enormous numbers of small, uniform-lifetime
objects: every Ty, every HirId, every Span, every MIR statement. A
general-purpose allocator’s per-object overhead (size class lookup, free
list management, coalescing) is pure waste for objects that are all freed
at once at the end of the compilation.
A bump allocator makes allocation two instructions and deallocation free.
Where it lives
compiler/rustc_arena/src/lib.rs—DroplessArena,TypedArena,declare_arena!.compiler/rustc_middle/src/arena.rs— the compiler’s main arena.compiler/rustc_hir/src/arena.rs— the HIR arena.
Takeaway
The assert_unchecked trick is the most transferable idea here: when you
maintain an alignment invariant by construction, tell the compiler about
it, and the alignment arithmetic disappears from the hot path. The
downward bump is a smaller win but the same species of trick — choose the
representation that makes the invariant easy to see and to state.