🎓 A1Learner
🔍

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?