rust-memory
Minimize allocations and cloning using references, Cow, and smart pointers. Use when optimizing hot paths or memory-constrained code.
When & Why to Use This Skill
This Claude skill provides expert-level patterns and guidelines for optimizing memory efficiency in Rust. It focuses on minimizing heap allocations and unnecessary cloning by leveraging advanced language features such as references, smart pointers (Arc, Rc, Box), the Cow (Copy-on-Write) type, and zero-copy data structures. It is specifically designed to help developers optimize performance-critical 'hot paths' and manage resources effectively in memory-constrained environments.
Use Cases
- Refactoring performance-critical code to eliminate unnecessary .clone() calls by utilizing ownership transfers or borrowing.
- Optimizing high-throughput data pipelines by replacing intermediate collection allocations with chained lazy iterators.
- Implementing memory-efficient string handling and path normalization using the Cow (Copy-on-Write) type to avoid allocations when data is unmodified.
- Reducing heap fragmentation and allocation overhead in microservices by pre-allocating collection capacities and using stack-allocated alternatives like SmallVec.
- Designing zero-copy buffer management systems using the Bytes crate for efficient network or file I/O operations.
| name | rust-memory |
|---|---|
| description | Minimize allocations and cloning using references, Cow, and smart pointers. Use when optimizing hot paths or memory-constrained code. |
Memory Efficiency
Patterns to minimize allocations and copies in Rust.
Avoid Unnecessary Cloning
// BAD: Cloning when not needed
fn process(data: Vec<String>) -> Vec<String> {
data.clone().iter().map(|s| s.to_uppercase()).collect()
}
// GOOD: Take ownership or borrow
fn process(data: Vec<String>) -> Vec<String> {
data.into_iter().map(|s| s.to_uppercase()).collect()
}
// GOOD: Return iterator for lazy evaluation
fn process(data: &[String]) -> impl Iterator<Item = String> + '_ {
data.iter().map(|s| s.to_uppercase())
}
Prefer &str over String
// BAD: Requires allocation
fn greet(name: String) {
println!("Hello, {}!", name);
}
// GOOD: Accepts any string-like type
fn greet(name: &str) {
println!("Hello, {}!", name);
}
// BEST: Generic for flexibility
fn greet(name: impl AsRef<str>) {
println!("Hello, {}!", name.as_ref());
}
Pre-allocate Collections
// BAD: Multiple reallocations as vector grows
let mut v = Vec::new();
for i in 0..1000 {
v.push(i);
}
// GOOD: Single allocation
let mut v = Vec::with_capacity(1000);
for i in 0..1000 {
v.push(i);
}
// Also works for String and HashMap
let mut s = String::with_capacity(4000);
let mut map = HashMap::with_capacity(100);
Copy-on-Write (Cow)
use std::borrow::Cow;
// Return borrowed if no modification needed, owned if modified
fn normalize_path(path: &str) -> Cow<'_, str> {
if path.contains("//") {
Cow::Owned(path.replace("//", "/"))
} else {
Cow::Borrowed(path)
}
}
// Usage - no allocation if already normalized
let normalized = normalize_path("/home/user"); // Borrowed
let fixed = normalize_path("/home//user"); // Owned
// Cow in structs
struct Config<'a> {
name: Cow<'a, str>,
}
impl<'a> Config<'a> {
fn new(name: &'a str) -> Self {
Self { name: Cow::Borrowed(name) }
}
fn with_prefix(mut self, prefix: &str) -> Self {
self.name = Cow::Owned(format!("{}{}", prefix, self.name));
self
}
}
Smart Pointers
use std::sync::Arc;
use std::rc::Rc;
// Arc - Shared ownership across threads (atomic reference counting)
let data = Arc::new(expensive_data);
let data_clone = Arc::clone(&data); // Cheap clone, shares data
// Rc - Single-threaded shared ownership
let data = Rc::new(expensive_data);
let data_clone = Rc::clone(&data);
// Box - Heap allocation for large data or recursive types
let large_data = Box::new([0u8; 1_000_000]);
// Arc::clone is explicit about what it does
// Prefer Arc::clone(&x) over x.clone() for clarity
Zero-Copy with Bytes
use bytes::Bytes;
// Bytes: Reference-counted, immutable bytes
let data = Bytes::from(vec![0u8; 1024]);
let slice = data.slice(0..512); // No copy, shares underlying buffer
// BytesMut for building buffers
use bytes::BytesMut;
let mut buf = BytesMut::with_capacity(1024);
buf.extend_from_slice(b"hello");
buf.extend_from_slice(b" world");
let frozen = buf.freeze(); // Convert to immutable Bytes
SmallVec for Small Collections
use smallvec::SmallVec;
// Stack-allocated if <= 8 items, heap otherwise
let mut results: SmallVec<[Result; 8]> = SmallVec::new();
results.push(Ok(1));
results.push(Ok(2));
// No heap allocation for common case
Avoid Intermediate Collections
// BAD: Creates intermediate Vec
let sum: i32 = data
.iter()
.filter(|&&x| x > 2)
.collect::<Vec<_>>() // Unnecessary allocation
.iter()
.map(|&&x| x * 2)
.sum();
// GOOD: Chain iterators, single pass
let sum: i32 = data
.iter()
.filter(|&&x| x > 2)
.map(|&x| x * 2)
.sum();
String Building
// BAD: Multiple allocations
let s = String::new() + "hello" + " " + "world";
// GOOD: Pre-allocate and push
let mut s = String::with_capacity(11);
s.push_str("hello");
s.push(' ');
s.push_str("world");
// GOOD: Use format! for clarity (single allocation)
let s = format!("hello {} world", name);
// BEST for loops: Join
let parts = vec!["hello", "world"];
let s = parts.join(" ");
Take and Replace
// Extract value without cloning
struct Container {
data: Option<Vec<u8>>,
}
impl Container {
fn take_data(&mut self) -> Option<Vec<u8>> {
self.data.take() // Replaces with None, returns original
}
}
// Replace and get old value
let mut value = 5;
let old = std::mem::replace(&mut value, 10);
assert_eq!(old, 5);
assert_eq!(value, 10);
Guidelines
- Use
&strinstead ofStringfor parameters - Pre-allocate with
with_capacity()when size is known - Use
Cowfor "maybe modified" returns - Prefer
Arc::clone(&x)overx.clone()for clarity - Use
Bytesfor zero-copy buffer sharing - Chain iterators instead of collecting intermediates
- Use
take()andreplace()to avoid cloning
Examples
See hercules-local-algo/src/pipeline/ for memory-efficient patterns.