How do I use SmallVec for efficient vector storage in Rust?
Walkthrough
The smallvec crate provides a SmallVec type that stores elements inline (on the stack) for small collections, automatically spilling to the heap when the collection grows beyond its inline capacity. This optimization is valuable when most of your vectors are small but you still need to handle the occasional large collection. It eliminates heap allocations for the common case, improving cache locality and reducing memory fragmentation. SmallVec is particularly useful in performance-critical code, embedded systems, and situations where allocation overhead matters.
Key concepts:
- Inline capacity — stack storage for N elements, specified as type parameter
- Spilling — automatic migration to heap when capacity is exceeded
- Array backing —
SmallVec<[T; N]>uses array[T; N]as backing storage - Zero overhead — no performance penalty when staying within inline capacity
- Vec-like API — familiar methods like
push,pop,iter, etc.
Code Example
# Cargo.toml
[dependencies]
smallvec = "1"use smallvec::SmallVec;
fn main() {
// Inline capacity for 4 elements
let mut vec: SmallVec<[i32; 4]> = SmallVec::new();
vec.push(1);
vec.push(2);
vec.push(3);
vec.push(4);
println!("Vec length: {}", vec.len());
}Creating SmallVec
use smallvec::SmallVec;
fn main() {
// Create empty SmallVec with inline capacity for 4 i32s
let mut vec: SmallVec<[i32; 4]> = SmallVec::new();
// Create with initial capacity (still inline-first)
let vec: SmallVec<[i32; 4]> = SmallVec::with_capacity(10);
// Create from a slice
let vec: SmallVec<[i32; 4]> = SmallVec::from_slice(&[1, 2, 3]);
// Create from an array (always inline)
let vec: SmallVec<[i32; 4]> = SmallVec::from([1, 2, 3, 4]);
// Create using smallvec! macro
let vec: SmallVec<[i32; 4]> = smallvec::smallvec![1, 2, 3];
// Create from_buf (takes ownership of array)
let vec: SmallVec<[i32; 4]> = SmallVec::from_buf([1, 2, 3, 4]);
println!("Created SmallVecs with various methods");
}Basic Operations
use smallvec::SmallVec;
fn main() {
let mut vec: SmallVec<[String; 2]> = SmallVec::new();
// Push elements
vec.push("hello".to_string());
vec.push("world".to_string());
println!("After 2 pushes: {:?}", vec);
// Still inline!
println!("Is inline: {}", !vec.spilled());
// Push beyond inline capacity
vec.push("overflow".to_string());
println!("After spill: {:?}", vec);
println!("Is spilled: {}", vec.spilled());
// Pop elements
let last = vec.pop();
println!("Popped: {:?}", last);
// Insert at position
vec.insert(1, "inserted".to_string());
println!("After insert: {:?}", vec);
// Remove at position
let removed = vec.remove(0);
println!("Removed: {:?}", removed);
// Clear
vec.clear();
println!("After clear: {:?}", vec);
}Detecting Spilling
use smallvec::SmallVec;
fn main() {
let mut vec: SmallVec<[i32; 3]> = SmallVec::new();
println!("Initial:");
println!(" len: {}", vec.len());
println!(" capacity: {}", vec.capacity());
println!(" spilled: {}", vec.spilled());
vec.push(1);
vec.push(2);
vec.push(3);
println!("\nAfter 3 pushes (at capacity):");
println!(" len: {}", vec.len());
println!(" capacity: {}", vec.capacity());
println!(" spilled: {}", vec.spilled());
vec.push(4);
println!("\nAfter 4th push (spilled):");
println!(" len: {}", vec.len());
println!(" capacity: {}", vec.capacity());
println!(" spilled: {}", vec.spilled());
}Using with Functions
use smallvec::SmallVec;
// Accept any SmallVec with inline capacity 4
fn process_smallvec(vec: &mut SmallVec<[i32; 4]>) {
for i in vec.iter_mut() {
*i *= 2;
}
}
// Return SmallVec to avoid heap allocation
fn get_coordinates() -> SmallVec<[f64; 3]> {
let mut coords = SmallVec::new();
coords.push(1.0);
coords.push(2.0);
coords.push(3.0);
coords
}
// Generic over smallvec size
fn print_vec<T, const N: usize>(vec: &SmallVec<[T; N]>)
where
T: std::fmt::Debug,
{
println!("Contents: {:?}", vec.as_slice());
}
fn main() {
let mut vec: SmallVec<[i32; 4]> = smallvec::smallvec![1, 2, 3];
process_smallvec(&mut vec);
println!("After processing: {:?}", vec);
let coords = get_coordinates();
println!("Coordinates: {:?}", coords);
let vec: SmallVec<[String; 2]> = smallvec::smallvec!["a".to_string()];
print_vec(&vec);
}Iteration
use smallvec::SmallVec;
fn main() {
let mut vec: SmallVec<[i32; 4]> = smallvec::smallvec![1, 2, 3, 4, 5];
// Immutable iteration
println!("Elements:");
for elem in vec.iter() {
println!(" {}", elem);
}
// Mutable iteration
for elem in vec.iter_mut() {
*elem *= 2;
}
println!("After doubling: {:?}", vec);
// Into iteration (consumes)
let vec: SmallVec<[String; 2]> = smallvec::smallvec!["a".to_string(), "b".to_string()];
for elem in vec.into_iter() {
println!("Owned: {}", elem);
}
}Slicing and Indexing
use smallvec::SmallVec;
fn main() {
let vec: SmallVec<[i32; 4]> = smallvec::smallvec![10, 20, 30, 40, 50];
// Index
println!("First: {}", vec[0]);
println!("Third: {}", vec[2]);
// Get slice
let slice: &[i32] = vec.as_slice();
println!("Slice: {:?}", slice);
// Get mutable slice
let mut vec: SmallVec<[i32; 4]> = smallvec::smallvec![1, 2, 3];
{
let slice = vec.as_mut_slice();
slice[0] = 100;
}
println!("After mutation: {:?}", vec);
// Slice patterns
match vec.as_slice() {
[first, rest @ ..] => println!("First: {}, Rest: {:?}", first, rest),
[] => println!("Empty"),
}
}Capacity Management
use smallvec::SmallVec;
fn main() {
let mut vec: SmallVec<[i32; 4]> = SmallVec::new();
// Reserve capacity
vec.reserve(10);
println!("After reserve(10): capacity = {}", vec.capacity());
// Push elements
for i in 0..15 {
vec.push(i);
}
println!("After 15 pushes: len = {}, capacity = {}", vec.len(), vec.capacity());
// Shrink to fit
vec.shrink_to_fit();
println!("After shrink_to_fit: capacity = {}", vec.capacity());
// Truncate
vec.truncate(5);
println!("After truncate(5): len = {}", vec.len());
// Reserve exact
vec.reserve_exact(10);
println!("After reserve_exact(10): capacity = {}", vec.capacity());
}Growing from Iterator
use smallvec::SmallVec;
fn main() {
// Collect into SmallVec
let vec: SmallVec<[i32; 4]> = (1..=10).collect();
println!("Collected: {:?}", vec);
println!("Spilled: {}", vec.spilled());
// Extend with iterator
let mut vec: SmallVec<[String; 2]> = smallvec::smallvec!["a".to_string()];
vec.extend(["b".to_string(), "c".to_string(), "d".to_string()]);
println!("Extended: {:?}", vec);
// From iterator
let vec: SmallVec<[i32; 3]> = SmallVec::from_iter([1, 2, 3]);
println!("From iter: {:?}", vec);
}Drain and IntoIter
use smallvec::SmallVec;
fn main() {
let mut vec: SmallVec<[i32; 4]> = smallvec::smallvec![1, 2, 3, 4, 5];
// Drain elements from index 1..4
let drained: Vec<i32> = vec.drain(1..4).collect();
println!("Drained: {:?}", drained);
println!("Remaining: {:?}", vec);
// Drain all
let mut vec: SmallVec<[i32; 4]> = smallvec::smallvec![10, 20, 30];
let all: Vec<i32> = vec.drain(..).collect();
println!("All drained: {:?}", all);
println!("Vec is now empty: {}", vec.is_empty());
// into_vec() converts to Vec (may avoid allocation if spilled)
let vec: SmallVec<[i32; 4]> = smallvec::smallvec![1, 2, 3, 4, 5];
let regular_vec: Vec<i32> = vec.into_vec();
println!("Regular Vec: {:?}", regular_vec);
}Into Inner
use smallvec::SmallVec;
fn main() {
// into_inner() returns the array if not spilled
let vec: SmallVec<[i32; 4]> = smallvec::smallvec![1, 2, 3];
match vec.into_inner() {
Ok(arr) => println!("Got array: {:?}", arr),
Err(vec) => println!("Was spilled, got back SmallVec with {} elements", vec.len()),
}
// Spilled case
let vec: SmallVec<[i32; 2]> = smallvec::smallvec![1, 2, 3, 4, 5];
match vec.into_inner() {
Ok(arr) => println!("Got array: {:?}", arr),
Err(vec) => println!("Was spilled, got back SmallVec with {} elements", vec.len()),
}
}Real-World: Token Processing
use smallvec::SmallVec;
#[derive(Debug, Clone)]
enum Token {
Identifier(String),
Number(i64),
Operator(char),
}
// Most expressions have few tokens, so SmallVec is perfect
fn tokenize(input: &str) -> SmallVec<[Token; 8]> {
let mut tokens = SmallVec::new();
for word in input.split_whitespace() {
let token = if let Ok(n) = word.parse::<i64>() {
Token::Number(n)
} else if word.len() == 1 && "+-*/".contains(word) {
Token::Operator(word.chars().next().unwrap())
} else {
Token::Identifier(word.to_string())
};
tokens.push(token);
}
tokens
}
fn main() {
let expr = "x + 42 * y";
let tokens = tokenize(expr);
println!("Tokens ({} items, spilled: {}):", tokens.len(), tokens.spilled());
for token in tokens.iter() {
println!(" {:?}", token);
}
}Real-World: Event Queue
use smallvec::SmallVec;
#[derive(Debug, Clone)]
enum Event {
MouseMove { x: i32, y: i32 },
Click { button: u8 },
KeyPress(char),
}
struct EventQueue {
// Most frames have few events, use SmallVec
events: SmallVec<[Event; 16]>,
}
impl EventQueue {
fn new() -> Self {
Self { events: SmallVec::new() }
}
fn push(&mut self, event: Event) {
self.events.push(event);
}
fn process(&mut self) {
for event in self.events.drain(..) {
match event {
Event::MouseMove { x, y } => println!("Mouse moved to ({}, {})", x, y),
Event::Click { button } => println!("Button {} clicked", button),
Event::KeyPress(c) => println!("Key pressed: {}", c),
}
}
}
}
fn main() {
let mut queue = EventQueue::new();
queue.push(Event::MouseMove { x: 100, y: 200 });
queue.push(Event::Click { button: 1 });
queue.push(Event::KeyPress('a'));
println!("Events: {} (spilled: {})", queue.events.len(), queue.events.spilled());
queue.process();
}Real-World: Path Segments
use smallvec::SmallVec;
// Most paths have few segments
struct Path {
segments: SmallVec<[String; 4]>,
}
impl Path {
fn new() -> Self {
Self { segments: SmallVec::new() }
}
fn from_str(s: &str) -> Self {
let segments = s.split('/')
.filter(|s| !s.is_empty())
.map(String::from)
.collect();
Self { segments }
}
fn push(&mut self, segment: &str) {
self.segments.push(segment.to_string());
}
fn pop(&mut self) -> Option<String> {
self.segments.pop()
}
fn to_string(&self) -> String {
let mut result = String::from("/");
for (i, segment) in self.segments.iter().enumerate() {
if i > 0 {
result.push('/');
}
result.push_str(segment);
}
result
}
}
fn main() {
let path = Path::from_str("/home/user/documents/file.txt");
println!("Path: {}", path.to_string());
println!("Segments: {} (spilled: {})", path.segments.len(), path.segments.spilled());
}Real-World: Stack-Based Calculator
use smallvec::SmallVec;
struct Calculator {
// Most calculations need few values on stack
stack: SmallVec<[f64; 8]>,
}
impl Calculator {
fn new() -> Self {
Self { stack: SmallVec::new() }
}
fn push(&mut self, value: f64) {
self.stack.push(value);
}
fn add(&mut self) -> Result<f64, &'static str> {
if self.stack.len() < 2 {
return Err("Need at least 2 values");
}
let b = self.stack.pop().unwrap();
let a = self.stack.pop().unwrap();
self.stack.push(a + b);
Ok(a + b)
}
fn mul(&mut self) -> Result<f64, &'static str> {
if self.stack.len() < 2 {
return Err("Need at least 2 values");
}
let b = self.stack.pop().unwrap();
let a = self.stack.pop().unwrap();
self.stack.push(a * b);
Ok(a * b)
}
fn result(&self) -> Option<f64> {
self.stack.last().copied()
}
}
fn main() {
let mut calc = Calculator::new();
// Calculate (3 + 4) * 2
calc.push(3.0);
calc.push(4.0);
calc.add().unwrap();
calc.push(2.0);
calc.mul().unwrap();
println!("Result: {}", calc.result().unwrap());
println!("Stack spilled: {}", calc.stack.spilled());
}Performance Comparison
use smallvec::SmallVec;
use std::time::Instant;
fn benchmark_vec(count: usize) -> std::time::Duration {
let start = Instant::now();
let mut total = 0;
for _ in 0..count {
let mut vec = Vec::new();
vec.push(1);
vec.push(2);
vec.push(3);
total += vec.len();
}
start.elapsed()
}
fn benchmark_smallvec(count: usize) -> std::time::Duration {
let start = Instant::now();
let mut total = 0;
for _ in 0..count {
let mut vec: SmallVec<[i32; 4]> = SmallVec::new();
vec.push(1);
vec.push(2);
vec.push(3);
total += vec.len();
}
start.elapsed()
}
fn main() {
let count = 1_000_000;
let vec_time = benchmark_vec(count);
let smallvec_time = benchmark_smallvec(count);
println!("Vec: {:?}", vec_time);
println!("SmallVec: {:?}", smallvec_time);
println!("Speedup: {:.2}x", vec_time.as_secs_f64() / smallvec_time.as_secs_f64());
}Comparison with Vec
use smallvec::SmallVec;
fn main() {
// Vec: always heap allocates
let regular_vec: Vec<i32> = vec![1, 2, 3];
println!("Vec: {:?}", regular_vec);
// SmallVec: stack-allocated for small collections
let small: SmallVec<[i32; 4]> = smallvec::smallvec![1, 2, 3];
println!("SmallVec (inline): {:?}", small);
println!("Spilled: {}", small.spilled());
// SmallVec grows beyond inline capacity
let large: SmallVec<[i32; 2]> = smallvec::smallvec![1, 2, 3, 4, 5];
println!("SmallVec (spilled): {:?}", large);
println!("Spilled: {}", large.spilled());
// Convert between
let vec: Vec<i32> = small.into_vec();
println!("Converted to Vec: {:?}", vec);
}Using with Generics
use smallvec::SmallVec;
// Generic function accepting SmallVec
fn sum<T, const N: usize>(vec: &SmallVec<[T; N]>) -> T
where
T: std::ops::Add<Output = T> + Default + Copy,
{
vec.iter().copied().fold(T::default(), |acc, x| acc + x)
}
// Return SmallVec from function
fn range_smallvec(start: i32, end: i32) -> SmallVec<[i32; 16]> {
(start..end).collect()
}
fn main() {
let vec: SmallVec<[i32; 4]> = smallvec::smallvec![1, 2, 3, 4, 5];
println!("Sum: {}", sum(&vec));
let range = range_smallvec(1, 10);
println!("Range: {:?}", range);
}Clone and Copy
use smallvec::SmallVec;
fn main() {
let original: SmallVec<[i32; 4]> = smallvec::smallvec![1, 2, 3];
// Clone
let cloned = original.clone();
println!("Cloned: {:?}", cloned);
// Clone from slice
let slice = &[10, 20, 30];
let from_slice = SmallVec::<[i32; 4]>::from_slice(slice);
println!("From slice: {:?}", from_slice);
// Note: SmallVec is not Copy even if T is Copy,
// because it may have spilled to heap
}Dropping and Memory Safety
use smallvec::SmallVec;
struct Droppable {
id: i32,
}
impl Drop for Droppable {
fn drop(&mut self) {
println!("Dropping {}", self.id);
}
}
fn main() {
println!("Creating SmallVec...");
{
let mut vec: SmallVec<[Droppable; 2]> = SmallVec::new();
vec.push(Droppable { id: 1 });
vec.push(Droppable { id: 2 });
vec.push(Droppable { id: 3 }); // Causes spill
println!("Going out of scope...");
}
println!("SmallVec dropped.");
}Draining with Range
use smallvec::SmallVec;
fn main() {
let mut vec: SmallVec<[i32; 4]> = smallvec::smallvec![1, 2, 3, 4, 5, 6, 7, 8];
// Drain specific range
let middle: Vec<i32> = vec.drain(2..5).collect();
println!("Drained middle: {:?}", middle);
println!("Remaining: {:?}", vec);
// Drain from index to end
let mut vec: SmallVec<[i32; 4]> = smallvec::smallvec![10, 20, 30, 40];
let tail: Vec<i32> = vec.drain(2..).collect();
println!("Drained tail: {:?}", tail);
println!("Remaining: {:?}", vec);
}Summary
SmallVec<[T; N]>stores N elements inline on the stack- Automatically spills to heap when capacity is exceeded
- Use
.spilled()to check if heap allocation occurred .into_inner()returns the array if not spilled.into_vec()converts toVec(may avoid allocation)- API mirrors
Vecfor easy migration - Perfect when most collections are small
- Use
smallvec!macro for literal construction - Significant performance gains in allocation-heavy code
- Common uses: token processing, event queues, path segments, temporary buffers
