| name | rust-memory-optimization |
| description | | Use when this capability is needed. |
Quick Navigation
Rust Memory Optimization Guide
Advanced strategies for reducing allocations, shrinking layout sizes, and boosting performance.
Core Rules & Patterns
1. Bounded Heap-Free Collections
For collections that are usually small, avoid standard vectors to bypass heap allocations.
SmallVec: Stack-allocated up to a threshold, overflows to the heap.
ArrayVec: Fixed-capacity array wrapper; never allocates on the heap.
ThinVec: Only takes a single pointer size when empty, reducing size in enum layouts.
use smallvec::{smallvec, SmallVec};
use arrayvec::ArrayVec;
let mut local_tags: SmallVec<[String; 4]> = smallvec![];
local_tags.push("Rust".to_string());
let mut coordinates: ArrayVec<f64, 8> = ArrayVec::new();
coordinates.push(42.0);
2. Small String Optimization (SSO)
Standard String consumes 24 bytes and allocates heap space immediately. Use CompactString for inline small strings (under 24 bytes) without allocating.
use compact_str::CompactString;
let username = CompactString::new("user_123");
3. Boxed Slices vs Vectors
If a collection's size is determined once at initialization and never changes, convert it from Vec<T> to Box<[T]> to save 8 bytes of stack capacity space.
let items_vec: Vec<i32> = vec![1, 2, 3];
let items_slice: Box<[i32]> = items_vec.into_boxed_slice();
4. Zero-Copy Patterns
Instead of cloning or allocating strings, hold references to existing memory using slices or crates like bytes::Bytes.
use bytes::Bytes;
pub struct Message {
pub payload: Bytes,
}
let original = Bytes::from(vec![0; 1024]);
let slice = original.slice(0..100);
5. Arena Allocation
For batch allocation where structures share a lifecycle (e.g. AST parsing), use arena allocators (bumpalo) to allocate items sequentially in a continuous region of memory and free them all at once.
use bumpalo::Bump;
let bump = Bump::new();
let string_ref = bump.alloc_str("dynamic string");
let int_ref = bump.alloc(42);
6. Collection Reuse
Reuse collection allocations in performance-critical loops with .clear() instead of re-instantiating.
for item in feed {
let mut batch = Vec::new();
batch.push(item);
}
let mut batch = Vec::with_capacity(100);
for item in feed {
batch.clear();
batch.push(item);
}
Layout Awareness
Use std::mem::size_of to understand data layout.
println!("User size: {}", std::mem::size_of::<User>());
Reorder fields to reduce padding only when the type is numerous or hot.
struct Record {
id: u64,
flags: u32,
kind: u8,
}
String Strategy
fn label<'a>(name: &'a str, fallback: &'a str) -> std::borrow::Cow<'a, str> {
if name.is_empty() { fallback.into() } else { name.into() }
}
Avoid format! in tight loops. Reuse buffers with clear().
Zero-Copy Parsing
Return slices into the input when the input outlives parsed data.
pub struct Header<'a> {
pub name: &'a str,
pub value: &'a str,
}
Do not force zero-copy when it creates unmanageable lifetimes for little gain.
ThinVec for Enum Payloads
When enums contain Vec variants, ThinVec reduces the enum size by storing only a pointer when empty:
use thin_vec::ThinVec;
enum Node {
Leaf(i64),
Children(Vec<Node>),
}
enum NodeOptimized {
Leaf(i64),
Children(ThinVec<Node>),
}
Cow for Conditional Ownership
use std::borrow::Cow;
pub struct Error {
message: Cow<'static, str>,
}
impl Error {
pub fn new(msg: &'static str) -> Self {
Self { message: Cow::Borrowed(msg) }
}
pub fn with_detail(msg: String) -> Self {
Self { message: Cow::Owned(msg) }
}
}
Arc vs Rc Trade-offs
use std::rc::Rc;
use std::sync::Arc;
let shared = Rc::new(large_data);
let shared = Arc::new(large_data);
Prefer Rc when data never crosses thread boundaries. Arc adds atomic reference counting overhead.
HashMap Memory Optimization
use hashbrown::HashMap;
let mut map: HashMap<String, i32> = HashMap::with_capacity(128);
map.reserve(expected_count);
use rustc_hash::FxHashMap;
let mut fast_map: FxHashMap<String, i32> = FxHashMap::default();
Stack vs Heap Decision Matrix
| Type | Stack Size | Heap Alloc | Use When |
|---|
String | 24 bytes | Yes | Owns mutable text |
&str | 16 bytes | No | Borrowed text, lifetime bound |
CompactString | 24 bytes | No (≤24 chars) | Short strings that own data |
Cow<str> | 32 bytes | Conditionally | Mostly static, rarely dynamic |
Box<str> | 16 bytes | Yes | Owned fixed string |
Vec<T> | 24 bytes | Yes | Growable collection |
SmallVec<[T; N]> | ~24+ bytes | Only if >N | Usually small, occasionally large |
Box<[T]> | 16 bytes | Yes | Fixed-size after construction |
ThinVec<T> | 8 bytes | Only if non-empty | Empty or small in enums |
Measuring Memory
use std::alloc::{GlobalAlloc, System, Layout};
struct TrackingAllocator;
unsafe impl GlobalAlloc for TrackingAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
System.alloc(layout)
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
System.dealloc(ptr, layout)
}
}
Use dhat crate for heap profiling in tests:
#[cfg(test)]
mod tests {
#[test]
fn memory_test() {
let _profiler = dhat::Profiler::new_heap();
}
}
Optimization Workflow
- Measure allocations and CPU first.
- Identify the hot path and expected data sizes.
- Pick the narrowest memory change.
- Benchmark before/after.
- Keep readability unless the win is meaningful.
Allocation Decision Rules
Vec<T>: general purpose growable collection.
SmallVec<[T; N]>: usually-small collection with occasional heap spill.
ArrayVec<T, N>: hard maximum size, no heap allocation.
Box<[T]>: fixed-size heap slice after construction.
Bytes: shared immutable byte buffers.
Bump: many same-lifetime allocations.
Cow<'a, T>: borrow unless mutation/ownership is required.
Anti-Patterns
let data = original.clone();
let data: Arc<[u8]> = original.into();
for item in items {
let s = format!("processing {}", item);
}
let mut buf = String::with_capacity(128);
for item in items {
buf.clear();
write!(buf, "processing {}", item).unwrap();
}
- Replacing every
Vec with SmallVec without measuring.
- Keeping arena references after reset.
- Cloning strings to satisfy ownership instead of shortening borrow scopes.
- Using
Arc as a default clone workaround.
- Optimizing layout for types created a handful of times.
- Using
Box<dyn Trait> when generics would eliminate the allocation entirely.
Review Prompt
When reviewing memory optimization, ask what was measured, which allocation is removed, what lifetime assumptions changed, and whether the new type increases stack size or API complexity.
Optimization Checklist
- Measure allocations before changing data structures.
- Preallocate collections only when capacity is known or bounded.
- Prefer borrowing and
Cow before cloning strings or buffers.
- Use arenas only when lifetimes are truly batch-scoped.
- Check stack growth when replacing heap allocations with inline storage.
- Re-benchmark after changes and keep the simpler version if gains are noise.
- Profile with
dhat or heaptrack to find actual allocation hotspots.
- Consider
hashbrown or rustc-hash for HashMap-heavy code.
References
Source: adxptived/Rust-Skills — distributed by TomeVault.