Allocators and Memory Layout

On this page

Allocators provide storage that satisfies size and alignment requirements, type rules determine which bytes constitute a valid value, and ownership determines when destruction and deallocation occur. Separating these layers is essential to understanding why "obtaining a non-null pointer" does not equate to already possessing a valid T.

Compute LayoutCheck size, alignment, and overflow1Allocate storage and check for failureSuccessfully obtained aligned, uninitialized memory2Initialize and use TWrite valid values, maintain ownership and access rules34Requesting bytes and creating a valid T are different stagesEnd lifetime and deallocate storageDestruct T if necessary; use matching allocator and LayoutFor typical business logic, prefer RAII containers like Box and Vec to avoid manually managing the full lifecycle.

Global Allocator and System Allocator

Rust does not specify "all platforms default to calling glibc malloc" as a language contract. System represents the platform's system allocator, using different infrastructure on different systems; default choices and static/dynamic library environments should refer to the target documentation. System

The following complete program explicitly selects System, without needing to hand-write a GlobalAlloc implementation just for forwarding:

use std::alloc::System;

#[global_allocator]
static GLOBAL: System = System;

fn main() {
    let values = vec![10, 20, 30];
    assert_eq!(values.iter().sum::<i32>(), 60);
}

Implementing GlobalAlloc is an unsafe responsibility: you must satisfy the allocation and deallocation contracts, and these methods must not unwind. Additionally, avoid using logging or containers inside the allocator that might trigger further allocations, as this could lead to infinite recursion.

Do not rely on "the source code writes Box::new, so it must call the allocator exactly once" to maintain program correctness. The optimizer may eliminate or alter allocations; allocation counts can be used for restricted observation but cannot serve as a safety invariant. GlobalAlloc

Layout: Size, Alignment, and Representable Range

Layout::new::<T>() derives the layout from a type, and Layout::array::<T>(n) checks array dimensions. from_size_align requires alignment to be non-zero and a power of two, and guarantees that the size, rounded up to the alignment, does not exceed isize::MAX.

Layout itself allows zero size, but the calling contract for GlobalAlloc::alloc requires a non-zero size. Do not treat "successfully creating a Layout" as "directly callable on any allocation API." Different allocation interfaces have different rules for zero sizes that must be checked separately. Layout

use std::alloc::Layout;

fn main() {
    assert!(Layout::from_size_align(16, 3).is_err());
    assert!(Layout::array::<u64>(usize::MAX).is_err());
    let first = Layout::from_size_align(1, 1).unwrap();
    let next = Layout::from_size_align(4, 4).unwrap();
    let (combined, offset) = first.extend(next).unwrap();
    let padded = combined.pad_to_align();
    assert_eq!(offset, 4);
    assert_eq!(padded.size(), 8);
    assert_eq!(padded.align(), 4);
}

extend returns the offset of the second part; do not blindly add field sizes without checking. pad_to_align pads the tail so that contiguous objects also satisfy alignment; this example combines storage using explicit Layout, it does not prove that arbitrary Rust structs use the same field order.

Why Tail Padding Affects Array Stride

Suppose a record first places a 4-byte field with alignment 4, followed by a 1-byte field. The second field's offset is 4, and the combined effective end is 5; if the next record is placed immediately at offset 5, its first field will not satisfy 4-byte alignment. Therefore, the overall stride needs to be padded to 8.

use std::alloc::Layout;
fn main() {
    let word = Layout::from_size_align(4, 4).unwrap();
    let tag = Layout::from_size_align(1, 1).unwrap();
    let (record, tag_offset) = word.extend(tag).unwrap();
    assert_eq!(tag_offset, 4);
    assert_eq!(record.size(), 5);
    assert_eq!(record.pad_to_align().size(), 8);
    assert_eq!(record.pad_to_align().align(), 4);
}

The 3 bytes of tail padding here are used to ensure the next element is aligned; they do not carry business fields. When encoding for external protocols, you should write out the fields, rather than sending or hashing the padding bytes as stable content. Layout arithmetic solves spatial arrangement; it does not solve whether padding is initialized, whether values are valid, or whether pointers can access them.

Raw Allocation: Check for Null First, Then Initialize

use std::alloc::{alloc, dealloc, handle_alloc_error, Layout};

fn main() {
    let layout = Layout::new::<u64>(); // Non-zero size
    // SAFETY: layout meets the requirements of alloc.
    let storage = unsafe { alloc(layout) };
    if storage.is_null() {
        handle_alloc_error(layout);
    }
    let pointer = storage.cast::<u64>();
    // SAFETY: Storage is large enough and aligned; exclusive access, write before read.
    let answer = unsafe {
        pointer.write(42);
        let value = pointer.read();
        // u64 requires no additional destruction; allocator, address, and Layout match the request.
        dealloc(storage, layout);
        value
    };
    assert_eq!(answer, 42);
}

Storage returned by alloc is uninitialized. Ordinary assignment may involve semantics of replacing old values; raw initialization should use appropriate write or MaybeUninit interfaces. For T containing resources, you must explicitly determine if read has moved ownership, if drop_in_place is still needed, to prevent leaks or double destruction.

dealloc only releases storage; it does not automatically call T::drop. This u64 example deliberately avoids adding fallible business logic between allocation and deallocation; more complex constructions need to handle partial initialization and panic paths, and should typically be handed over to Box, Vec, or specialized RAII wrappers.

Slot 0Valid TSlot 1Valid TSlot 2Uninitialized SpaceSlot 3Uninitialized SpaceSlot 4Uninitialized Spacelen = 2: Allowed to read and destructVec's capacity boundary and valid element boundary must be separatedcapacity = 5: Available storage, does not mean 5 Ts existThe diagram assumes a capacity of 5; read the specific capacity from the allocation API, do not infer it from the request value.

Vec: Initialize First, Then Extend Valid Length

Vec::with_capacity reserves space, but len remains 0; you cannot directly read empty slots via indexing. spare_capacity_mut exposes the spare part as [MaybeUninit<T>]; after writing, you must still update len to bring these values into Vec's access and destruction responsibilities. Vec spare capacity

fn main() {
    let mut values = Vec::<u32>::with_capacity(5);
    assert_eq!(values.len(), 0);
    assert!(values.capacity() >= 5);
    {
        let spare = values.spare_capacity_mut();
        spare[0].write(10);
        spare[1].write(20);
    }
    // SAFETY: Slots 0 and 1 have been written with valid u32, and 2 does not exceed capacity.
    unsafe { values.set_len(2); }
    assert_eq!(values, [10, 20]);
    values.push(30);
    assert_eq!(values, [10, 20, 30]);
}

This is a low-level example for understanding initialization boundaries; ordinary appending via push is easier to maintain. Do not set len first and then initialize items one by one: if a panic occurs midway, Vec may attempt to destruct Ts that are not yet valid. Conversely, if you write a String but never count it in len, Vec won't know to destruct it, potentially leaking its resources.

When batch construction has fallible steps, prefer letting the container take ownership of completed objects item by item. The following returns an error at the second element; the first object already placed in the Vec will be automatically released:

use std::cell::Cell;
use std::rc::Rc;
struct Tracked(Rc<Cell<usize>>);
impl Drop for Tracked {
    fn drop(&mut self) { self.0.set(self.0.get() + 1); }
}
fn build(drops: &Rc<Cell<usize>>) -> Result<Vec<Tracked>, &'static str> {
    let mut items = Vec::with_capacity(3);
    for index in 0..3 {
        if index == 1 { return Err("Construction failed"); }
        items.push(Tracked(Rc::clone(drops)));
    }
    Ok(items)
}
fn main() {
    let drops = Rc::new(Cell::new(0));
    assert!(build(&drops).is_err());
    assert_eq!(drops.get(), 1);
}

The value of RAII lies in allowing error paths and normal paths to share ownership rules. If you truly need raw batch initialization, a dedicated guard should record the number of initialized items and only destruct that prefix upon early exit; you cannot apply the destruction logic of the full array to partially valid storage.

Allocate Box First, Then Initialize Value

fn main() {
    let mut storage = Box::<u64>::new_uninit();
    storage.write(42);
    // SAFETY: The previous line fully initialized the unique u64, and it hasn't been invalidated since.
    let value: Box<u64> = unsafe { storage.assume_init() };
    assert_eq!(*value, 42);
}

Box::new_uninit returns Box<MaybeUninit<T>>; assume_init relies on the caller proving that T is valid. Allocating but not initializing and then deallocating this Box is allowed, but MaybeUninit will not automatically call destructors for manually written Ts inside it; responsibility must be correctly transferred after initialization. Zero-filling cannot replace this proof: all zeros are not a valid representation for any arbitrary T. Box::new_uninit

Capacity, initialization and len are distinct

Treat len as the container’s promise of valid values: it controls indexing and which elements are dropped. Moving the initialization and len boundaries separately exposes two different errors: growing len too early asserts nonexistent valid values; writing a String without transferring responsibility can omit its destructor. Ordinary push combines these steps safely.

Preparing the visual
Capacity, initialization and len are distinct

Every slot below len must be valid; initialized Strings beyond len are not automatically dropped by Vec.

Global Replacement and Per-Container Allocators Are Not the Same API

GlobalAlloc and #[global_allocator] are stable interfaces. The standard library Allocator trait and many new_in interfaces that select allocators per container are still marked as experimental under allocator_api in the documentation reviewed here; do not directly call nightly examples stable Rust usage. Allocator

Choose an allocator based on fragmentation, latency, thread contention, and workload measurements, not based on name promises of being universally faster. For predictable batch objects, first consider reducing allocation counts, reserving capacity, or changing data structures.

Reserved Capacity, Freed Capacity, and Process Memory Are Not the Same Metric

Operation or MetricWhat you can judge from itWhat you cannot directly infer
Vec::clearElements have been removed and destructed according to rules, len is zeroCapacity has been returned to the allocator
drop(Vec)Container releases the storage it ownsOS RSS drops by the same number of bytes immediately
try_reserveCan handle capacity overflow or allocation failure ResultInput scale already fits business resource budget
shrink_to_fitRequest to shrink excess capacityCapacity definitely equals len, memory definitely returned to OS immediately

Allocators may cache freed blocks, and pages may mix still-alive objects, so RSS differs from the valid bytes owned by objects. When analyzing "leaks," distinguish between continuously growing reachable objects, containers retaining capacity, allocators retaining, and the definitions used by system memory metrics; observing RSS not dropping once is insufficient to locate responsibility.

Before reserving the declared input quantity, verify business limits, then use try_reserve to handle resource failures. Even if size arithmetic does not overflow, malicious or erroneous input may still require unacceptable capacity. Generic containers must also consider zero-sized types: their capacity semantics cannot be directly converted to actual requested bytes.

What repr and Pin Guarantee Respectively

repr(C) constrains struct layout to follow corresponding C layout rules, but does not automatically recursively change the representation of field types; repr(align(N)) increases alignment, which may also increase padding. Target ABI, field valid values, and endianness still need consideration; do not directly treat struct memory as a cross-platform serialization format.

Pin expresses address-related contracts; it does not determine the allocator, nor does it automatically make a type immovable. For Unpin types, you can still safely obtain a movable &mut T through safe interfaces; a self-referential struct containing only String and raw pointers is typically still automatically Unpin, and wrapping it in Pin<Box<_>> is not sufficient to make it safe.

Self-referential abstractions require initialization at stable addresses, choosing appropriate !Unpin designs (such as PhantomPinned), restricting projections and moves, and adhering to destruction contracts. What is fixed is the pointee; the Box or Pin wrapper holding it can still move. See async/await for related mechanisms of asynchronous Futures, and Raw Pointers and FFI for raw access contracts.

Pin Does Not Prohibit Ordinary Field Modification

use std::pin::Pin;
fn main() {
    let mut pinned = Box::pin(String::from("old"));
    let text: &mut String = Pin::as_mut(&mut pinned).get_mut();
    let old = std::mem::replace(text, String::from("new"));
    assert_eq!(old, "old");
    assert_eq!(pinned.as_ref().get_ref(), "new");
}

String is Unpin, so safe interfaces allow replacing it. Even if changed to a custom type with PhantomPinned, it only restricts relevant move entry points; the initialization order of raw pointer targets, whether projections allow moving fields, and whether contracts are maintained during destruction remain the responsibility of the abstraction author. First clarify which address relationships must be maintained, then decide if Pin is needed; do not treat Pin as a universal "heap object hardening."

References