Rust Fewer Allocations
Fewer allocations means asking the heap for memory less often. Allocation costs time every single time.
Watch a Vec grow
Push one by one and the Vec keeps buying bigger memory and copying everything over:
let mut v: Vec<u32> = Vec::new();
for i in 0..20 { v.push(i); }
println!("capacity after 20 pushes = {}", v.capacity());Reserve once, up front
Know the size before you start? Buy the memory once and never grow:
let mut v: Vec<u32> = Vec::with_capacity(100);
for i in 0..100 { v.push(i); }Reuse the old buffer
clone_from copies new contents into the old allocation instead of buying a new one:
let mut v1: Vec<u32> = Vec::with_capacity(99);
let v2: Vec<u32> = vec![1, 2, 3];
v1.clone_from(&v2);
println!("capacity still = {}", v1.capacity());SmallVec: tiny lists skip the heap
SmallVec keeps the first 4 items inside itself — spilled() stays false until the heap is needed:
// smallvec = "1"
use smallvec::SmallVec;
let mut v: SmallVec<[u32; 4]> = SmallVec::new();
v.push(10); v.push(20); v.push(30);
println!("spilled to heap = {}", v.spilled());Cow: borrow until you must write
Cow ("clone on write") borrows its data and only copies when the code actually writes to it:
use std::borrow::Cow;
fn shout(input: &str) -> Cow<'_, str> {
if input.chars().all(|c| !c.is_lowercase()) { Cow::Borrowed(input) }
else { Cow::Owned(input.to_uppercase()) }
}Example
// a tiny allocator wrapper that counts allocations
use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering};
static ALLOCS: AtomicUsize = AtomicUsize::new(0);
struct Counting;
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
ALLOCS.fetch_add(1, Ordering::Relaxed);
unsafe { System.alloc(layout) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { unsafe { System.dealloc(ptr, layout) } }
}
#[global_allocator]
static GLOBAL: Counting = Counting;
fn main() {
println!("counting allocations");
ALLOCS.store(0, Ordering::SeqCst);
let mut v: Vec<u32> = Vec::new();
for i in 0..100 { v.push(i); }
let grew = ALLOCS.load(Ordering::SeqCst);
std::hint::black_box(&v); drop(v);
ALLOCS.store(0, Ordering::SeqCst);
let mut w: Vec<u32> = Vec::with_capacity(100);
for i in 0..100 { w.push(i); }
let reserved = ALLOCS.load(Ordering::SeqCst);
std::hint::black_box(&w); drop(w);
ALLOCS.store(0, Ordering::SeqCst);
let mut total = 0;
for i in 0..3 { let s = format!("item {i}"); total += s.len(); }
let fresh = ALLOCS.load(Ordering::SeqCst);
ALLOCS.store(0, Ordering::SeqCst);
let mut buf = String::new();
for word in ["bbb", "aa", "c"] { buf.clear(); buf.push_str(word); total += buf.len(); }
let reused = ALLOCS.load(Ordering::SeqCst);
std::hint::black_box(total);
println!("push 100, no reserve = {grew} allocations");
println!("push 100, reserved = {reserved} allocation");
println!("three fresh strings = {fresh} allocations");
println!("one reused buffer = {reused} allocation");
}Key points
- Allocation: asking the heap for a block of memory
- Giving the block back costs time too
- A Vec grows by doubling
- Capacities run 0, 4, 8, 16, 32…
- Every growth copies all the elements into the new block
- Use SmallVec (from the smallvec crate) for lists that are usually tiny
- The first few elements live inside the value itself
- The heap is only touched when it spills
- Borrow first
- A function that takes &[T] or &str never forces the caller to allocate
- A function that takes Vec<T> or String often does
- format! builds a fresh String every call — one allocation per call
- The heap is where Vec, String and Box keep their data.
- Fewer allocations means fewer trips to the heap for memory.
In this example, a counting allocator watches every heap request. Pushing 100 items into a fresh Vec allocates 6 times. The capacities run 4, 8, 16, 32, 64, 128. Reserving first allocates exactly once. Three format! calls allocate three times — one per fresh String — while a single reused buffer, cleared between words, allocates once. The habits this lesson taught, counted and proven. Rust Cow: Borrow or Build, earlier in the Beginner course, works this same borrow-or-build pattern end to end, with into_owned and to_mut.
Note
Do not chase every allocation
Count first, fix the hot ones, and leave the rest readable
Try it Yourself
Edit the code, then press Run.
Running is simulated · Rust Playground
Exercise
A Vec will end up with exactly 500 items. What removes the repeated growing?