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:

  1. Inline capacity — stack storage for N elements, specified as type parameter
  2. Spilling — automatic migration to heap when capacity is exceeded
  3. Array backingSmallVec<[T; N]> uses array [T; N] as backing storage
  4. Zero overhead — no performance penalty when staying within inline capacity
  5. 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 to Vec (may avoid allocation)
  • API mirrors Vec for 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