Rust Performance Engineering

Allocation Patterns

In high-throughput Rust services, parsers, and data pipelines, calling the global heap allocator thousands of times per second inside an inner loop is frequently the dominant CPU bottleneck. Applying high-performance allocation patterns—pre-allocation (with_capacity), buffer reuse (clear()), clone-in-place (clone_from), and arena/slab allocation—reduces heap traffic by orders of magnitude.

Anti-Pattern (Allocating & Dropping on Every Loop Iteration):
  for line in lines {
      let mut buf = Vec::new();  // <--- Allocates & grows 1 -> 4 -> 8 -> 16...
      process(line, &mut buf);
  } // <--- Frees `buf` on every single iteration! (Thousands of malloc/free calls)

High-Performance Pattern (Hoisted Buffer + `.clear()`):
  let mut buf = Vec::with_capacity(256); // <--- Single allocation before loop!
  for line in lines {
      buf.clear(); // <--- Sets `len = 0` in O(1) while retaining full heap `capacity`!
      process(line, &mut buf);
  } // <--- Zero allocations inside the loop once warmed up!

Four Essential Allocation Optimization Patterns

Pattern Standard Library API How It Saves Allocations
1. Capacity Pre-Allocation Vec::with_capacity(n), String::with_capacity(n), HashMap::with_capacity(n), vec.reserve(n) Allocates the exact final buffer once instead of geometric doubling (0 -> 4 -> 8 -> 16 -> 32...), eliminating intermediate realloc copies.
2. Loop Buffer Hoisting (clear) vec.clear(), string.clear(), map.clear() Resets len to 0 while keeping the underlying heap buffer (capacity) intact for the next iteration.
3. Reusing Destination Buffer on Clone target.clone_from(&source) Instead of *target = source.clone() (which drops target's buffer and allocates a brand-new buffer), clone_from reuses target's existing heap capacity!
4. Exact Size Hints in Iterator::collect ExactSizeIterator / Iterator::size_hint(&self) -> (usize, Option<usize>) Implementing size_hint on custom iterators lets .collect::<Vec<_>>() pre-allocate the exact buffer in a single call.

Demonstrating Buffer Reuse and clone_from

pub fn process_batches_reusing_buffer(batches: &[&[u32]]) -> Vec<u64> {
    let mut results = Vec::with_capacity(batches.len());
    // Hoist the scratch buffer outside the loop:
    let mut scratch: Vec<u32> = Vec::new();

    for &batch in batches {
        // `clear()` resets `len` to 0 in O(1) while preserving `scratch.capacity()`!
        scratch.clear();
        scratch.extend(batch.iter().copied().filter(|&x| x > 10));
        scratch.sort_unstable();

        let sum: u64 = scratch.iter().copied().map(u64::from).sum();
        results.push(sum);
    }

    println!(
        "Scratch final len={}, retained capacity={}",
        scratch.len(),
        scratch.capacity()
    );
    results
}

fn main() {
    let inputs: &[&[u32]] = &[
        &[5, 40, 12, 8, 25],
        &[100, 3, 50, 20, 60, 15],
        &[1, 2, 30],
    ];

    let sums = process_batches_reusing_buffer(inputs);
    assert_eq!(sums, vec![77, 245, 30]);

    // Demonstrating `clone_from` reusing existing String capacity:
    let mut reusable_name = String::with_capacity(128);
    let source = String::from("reuses_existing_128_byte_buffer");
    reusable_name.clone_from(&source);
    assert_eq!(reusable_name.capacity(), 128);
    println!("clone_from preserved capacity={}: {reusable_name}", reusable_name.capacity());
}
Scratch final len=1, retained capacity=5
clone_from preserved capacity=128: reuses_existing_128_byte_buffer

Arena / Bump Allocation for Request-Scoped Objects

When constructing complex graph structures or ASTs where thousands of nodes share the exact same lifetime (for example, all AST nodes for a single file parse, or all temporary strings for a single HTTP request), a bump / arena allocator (such as the bumpalo or typed-arena crate) allocates objects by incrementing a pointer inside a contiguous memory chunk in $O(1)$ time (~2 CPU instructions) and frees the entire arena at once when the request completes.