Skip to content
rusthacks.dev
Performance

Reducing Allocations in Rust and Finding the Hot Ones

Find where a Rust program allocates with a profiler or an allocation counter, then remove the hot ones: reuse buffers, borrow, pre-size and avoid format in loops.

3 min read
Summarize in AI
Flat illustration for the post “Reducing Allocations in Rust and Finding the Hot Ones”, composed around memory.

Allocation is rarely the slowest thing a program does, and it is the thing most often hiding in the middle of a hot loop. Each one is a few dozen nanoseconds plus a free, plus a cache miss when the memory is cold. A million allocations a second is not remarkable, and ten million is a profile that is mostly malloc. This post shows how to count them and what to change. It follows the profiling guide.

Rust 1.90, edition 2024.

Count them exactly

Wrap the global allocator with a counter:

rust
use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering};

struct Counting;
static ALLOCS: AtomicUsize = AtomicUsize::new(0);

unsafe impl GlobalAlloc for Counting {
    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
        ALLOCS.fetch_add(1, Ordering::Relaxed);
        // SAFETY: forwarding to the system allocator with the caller's layout.
        unsafe { System.alloc(layout) }
    }
    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
        // SAFETY: ptr and layout come from a matching `alloc` call.
        unsafe { System.dealloc(ptr, layout) }
    }
}

#[global_allocator]
static A: Counting = Counting;

fn allocations<R>(f: impl FnOnce() -> R) -> (R, usize) {
    let before = ALLOCS.load(Ordering::Relaxed);
    let r = f();
    (r, ALLOCS.load(Ordering::Relaxed) - before)
}

Wrap a function in allocations(|| ...) to see its count. Do it in a test or a small binary, not production. Crates such as dhat give per call-site totals; see the memory measurement posts for the heap profilers.

The usual sources

rust
fn slow(words: &[&str]) -> Vec<String> {
    let mut out = Vec::new();
    for w in words {
        let line = format!("{}!", w);         // 1 allocation per word
        out.push(line.to_uppercase());        // 1 more per word
    }
    out
}

For N words that is about 2N allocations plus the Vec regrowth. Rewrite:

rust
fn faster(words: &[&str]) -> Vec<String> {
    let mut out = Vec::with_capacity(words.len());   // one allocation
    for w in words {
        let mut s = w.to_uppercase();                // one per word, unavoidable here
        s.push('!');
        out.push(s);
    }
    out
}

Allocation count drops from roughly 2N + log N to N + 1. If the caller only needs to write the result somewhere, passing a &mut String or a Write target removes the per word String entirely.

Reuse

rust
let mut buf = String::new();
for item in items {
    buf.clear();
    use std::fmt::Write;
    write!(buf, "{}:{}", item.id, item.name).unwrap();
    sink(&buf);
}

After the first few iterations, buf has the capacity it needs and no further allocation happens. See reusing buffers.

Borrow

Storing &str slices of the input instead of String copies avoids the allocation and the copy. The cost is a lifetime parameter; for parsers that return slices of their input it is worth it. Cow<str> handles the sometimes-copy case.

Small values

Short strings and vectors that stay small fit inline with smallvec or compact_str, trading a larger stack object for no heap use; see small string and small vec optimizations.

Measure the effect

A counting allocator says allocations dropped; a benchmark says whether time did. Check both. Time rarely falls in proportion. The Rust Performance Book has a dedicated chapter on heap allocations.

Share

Keep reading

Flat illustration for the post “Stale While Revalidate Caching in Rust Services Explained”, composed around perf.
Performance

Stale While Revalidate Caching in Rust Services Explained

Serve the cached value immediately, even if slightly old, and refresh it in the background. Learn the idea, the HTTP Cache-Control directives and an in-process implementation with a freshness window and a grace window.

4 min read

Get new posts by email

We send one email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.