Rust Allocation Patterns
Rust Allocation Patterns
Expert Rust code follows a strict allocation avoidance hierarchy: borrow → Cow → owned. The fastest allocation is the one that doesn't happen. The pragmatic high-level-rust style inverts this for productivity — see the closing section on the high-level fallback.
The hierarchy
1. Borrow first
Accept &str and &[T] in function parameters. No allocation, no copying, maximum flexibility. This is the default.
2. Cow for conditional ownership
When a function usually returns input unchanged but sometimes modifies it, Cow<str> avoids allocation on the common path:
fn escape_html(input: &str) -> Cow<str> {
if !input.contains(['<', '>', '&']) {
Cow::Borrowed(input) // Zero allocation — most common path
} else {
Cow::Owned(input.replace('<', "<").replace('>', ">"))
}
}
3. Owned only when necessary
Allocate String, Vec<T>, Box<T> only when data must outlive the current scope or be mutated independently. Prefer String::with_capacity() when the size is known or estimable.
Buffer reuse
In loops, reuse allocations instead of creating new ones each iteration. String::with_capacity() then clear() reuses the underlying buffer — the allocation happens once, not per iteration. The same applies to Vec::clear().
SmallVec and stack allocation
smallvec<[T; N]> stores up to N elements inline on the stack before falling back to heap allocation. Ideal for collections that are usually small but occasionally grow.
Arena allocation
bumpalo provides bump allocation (~2 ns per allocation) with instant bulk deallocation. Ideal for phase-oriented work: per-request processing in web servers, AST nodes in compilers, parse trees. See also slotmap for generational-index arenas with stable handles.
Niche optimization
The Rust compiler exploits invalid bit patterns to store enum discriminants without additional space:
| Type | Size | Why |
|---|---|---|
Option<Box<T>> |
pointer-sized | null represents None |
Option<NonZeroU32> |
4 bytes | zero represents None |
Option<bool> |
1 byte | uses unused bit pattern |
Option<Option<bool>> |
1 byte | still fits in unused patterns |
Option<char> |
4 bytes | 0x00110000 (past max Unicode) for None |
For large enums, Box the largest variant to prevent it from inflating all variants — Clippy's large_enum_variant lint catches this automatically.
Zero-cost abstractions require release mode
Iterator chains compile to assembly identical to hand-written C loops — but only in release mode. Debug builds can be 10–50x slower. Verify zero-cost claims with cargo asm or Godbolt. Hidden costs to watch for: dyn Trait (vtable dispatch), Box<dyn Fn> (allocates), array indexing (bounds checks that iterators elide).
The high-level fallback
When productivity matters more than the last 20% of throughput, high-level-rust argues for inverting the hierarchy: own everything, clone freely, and rely on cheap-clone primitives to keep the cost bounded. Arc<T> becomes the default carrier; clones are reference bumps; mutation goes through Arc::make_mut or replacement. Persistent collections (CHAMP-backed im::Vector/HashMap) absorb the cost of "small edit to large collection."
The discipline this requires is real: Rust spells every clone the same way, but the cost gap between an Arc bump and a deep Vec<HashMap<String, String>> walk spans five orders of magnitude. See clone-cost-stratification for the strata and the patterns that keep clones cheap. The hierarchy on this page is still the right answer for hot paths and library code; the high-level inversion is for application code where measurement says you have the headroom.
Related pages
See expert-rust-design for the full design philosophy, rust-memory-allocators for global allocator choices, and bumpalo for arena allocation details. For the pragmatic counterpoint, see high-level-rust and clone-cost-stratification.
Linked from
Sources
- Raw/Rust/What A+ Rust design actually looks like.md
- Raw/Rust/High-Level Rust: Getting 80% of the Benefits with 20% of the Pain.md