How do I optimize small collections with smallvec in Rust?

Walkthrough

The smallvec crate provides a SmallVec type that stores elements inline (on the stack) up to a certain capacity, only spilling to the heap when that capacity is exceeded. This optimization eliminates heap allocations for small collections, improving performance and reducing memory fragmentation. SmallVec is particularly useful when you know most of your collections will be small, but need to handle larger cases gracefully. It's used extensively in performance-critical code like compilers, game engines, and web browsers.

Key concepts:

  1. SmallVec<[T; N]> — stores up to N elements inline, spills to heap for more
  2. SmallVec::new() — creates an empty SmallVec
  3. smallvec![] — macro for creating SmallVecs with initial values
  4. push() — adds elements, spilling to heap if needed
  5. drain() / into_iter() — consume the collection

Code Example

# Cargo.toml
[dependencies]
smallvec = "1.13"
use smallvec::SmallVec;
 
fn main() {
    // Store up to 4 integers inline
    let mut vec: SmallVec<[i32; 4]> = SmallVec::new();
    
    // These are stored on the stack (no heap allocation)
    vec.push(1);
    vec.push(2);
    vec.push(3);
    vec.push(4);
    
    println!("Inline: {:?}", vec);
    println!("On stack: {}", !vec.spilled());
    
    // This will spill to the heap
    vec.push(5);
    println!("After spill: {:?}", vec);
    println!("Spilled: {}", vec.spilled());
}

Basic Usage

use smallvec::SmallVec;
 
fn main() {
    // Create with inline capacity for 3 strings
    let mut names: SmallVec<[String; 3]> = SmallVec::new();
    
    names.push("Alice".to_string());
    names.push("Bob".to_string());
    names.push("Carol".to_string());
    
    println!("Names: {:?}", names);
    println!("Capacity: {}", names.capacity());
    println!("Inline: {}", !names.spilled());
}

Using the smallvec! Macro

use smallvec::{smallvec, SmallVec};
 
fn main() {
    // Create with initial values
    let nums: SmallVec<[i32; 4]> = smallvec![1, 2, 3];
    println!("Numbers: {:?}", nums);
    
    // Create empty
    let empty: SmallVec<[i32; 4]> = smallvec![];
    println!("Empty: {:?}", empty);
    
    // Create with specific capacity
    let with_capacity: SmallVec<[String; 2]> = SmallVec::with_capacity(5);
    println!("Capacity: {}", with_capacity.capacity());
}

Checking Stack vs Heap

use smallvec::SmallVec;
 
fn main() {
    let mut vec: SmallVec<[i32; 3]> = SmallVec::new();
    
    println!("=== Adding elements ===");
    
    for i in 1..=5 {
        vec.push(i);
        println!(
            "After push({}): len={}, capacity={}, spilled={}",
            i, vec.len(), vec.capacity(), vec.spilled()
        );
    }
    
    // Check if inline
    if vec.spilled() {
        println!("Data is on the heap");
    } else {
        println!("Data is on the stack");
    }
}

Conversion from Vec

use smallvec::SmallVec;
 
fn main() {
    // From Vec to SmallVec
    let regular_vec = vec![1, 2, 3, 4, 5];
    let small: SmallVec<[i32; 3]> = SmallVec::from_vec(regular_vec);
    println!("From Vec: {:?}", small);
    println!("Spilled: {}", small.spilled());
    
    // From slice
    let slice = &[10, 20, 30][..];
    let from_slice: SmallVec<[i32; 4]> = SmallVec::from_slice(slice);
    println!("From slice: {:?}", from_slice);
    
    // From SmallVec to Vec
    let small_vec: SmallVec<[i32; 2]> = smallvec![1, 2, 3, 4];
    let regular: Vec<i32> = small_vec.into_vec();
    println!("Into Vec: {:?}", regular);
}

IntoIter and Drain

use smallvec::{smallvec, SmallVec};
 
fn main() {
    let vec: SmallVec<[i32; 4]> = smallvec![1, 2, 3, 4, 5];
    
    // Iterate by value
    let sum: i32 = vec.clone().into_iter().sum();
    println!("Sum: {}", sum);
    
    // Drain elements
    let mut vec2: SmallVec<[String; 2]> = smallvec!["a".to_string(), "b".to_string(), "c".to_string()];
    let drained: Vec<String> = vec2.drain(..).collect();
    println!("Drained: {:?}", drained);
    println!("Empty: {:?}", vec2);
    
    // Drain a range
    let mut vec3: SmallVec<[i32; 4]> = smallvec![1, 2, 3, 4, 5];
    let middle: Vec<i32> = vec3.drain(1..4).collect();
    println!("Middle: {:?}", middle);
    println!("Remaining: {:?}", vec3);
}

Accessing Elements

use smallvec::{smallvec, SmallVec};
 
fn main() {
    let mut vec: SmallVec<[i32; 4]> = smallvec![10, 20, 30];
    
    // Index access
    println!("First: {}", vec[0]);
    println!("Second: {}", vec[1]);
    
    // get() returns Option
    if let Some(&val) = vec.get(2) {
        println!("Third: {}", val);
    }
    
    // Mutable access
    vec[0] = 100;
    println!("After mutation: {:?}", vec);
    
    // first() and last()
    println!("First: {:?}", vec.first());
    println!("Last: {:?}", vec.last());
    
    // Iteration
    for (i, &val) in vec.iter().enumerate() {
        println!("Index {}: {}", i, val);
    }
}

Removing Elements

use smallvec::{smallvec, SmallVec};
 
fn main() {
    let mut vec: SmallVec<[i32; 4]> = smallvec![1, 2, 3, 4, 5];
    
    // pop() removes last
    let last = vec.pop();
    println!("Popped: {:?}", last);
    println!("After pop: {:?}", vec);
    
    // remove() at index
    vec.remove(1);
    println!("After remove(1): {:?}", vec);
    
    // swap_remove() - O(1) but changes order
    vec.swap_remove(0);
    println!("After swap_remove(0): {:?}", vec);
    
    // truncate
    let mut vec2: SmallVec<[i32; 4]> = smallvec![1, 2, 3, 4, 5];
    vec2.truncate(3);
    println!("After truncate(3): {:?}", vec2);
    
    // clear
    vec2.clear();
    println!("After clear: {:?}", vec2);
}

Inserting at Specific Positions

use smallvec::{smallvec, SmallVec};
 
fn main() {
    let mut vec: SmallVec<[i32; 4]> = smallvec![1, 3, 5];
    
    // Insert at index
    vec.insert(1, 2);
    println!("After insert(1, 2): {:?}", vec);
    
    vec.insert(3, 4);
    println!("After insert(3, 4): {:?}", vec);
    
    // Extend from another iterator
    vec.extend([6, 7, 8]);
    println!("After extend: {:?}", vec);
    
    // Extend from slice
    let more = &[9, 10];
    vec.extend_from_slice(more);
    println!("After extend_from_slice: {:?}", vec);
}

Resizing and Reserving

use smallvec::{smallvec, SmallVec};
 
fn main() {
    let mut vec: SmallVec<[i32; 4]> = smallvec![1, 2];
    
    // Reserve additional capacity
    vec.reserve(10);
    println!("Capacity after reserve: {}", vec.capacity());
    
    // Resize (fill with default)
    vec.resize(5, 0);
    println!("After resize(5, 0): {:?}", vec);
    
    // Resize with closure
    vec.resize_with(8, Default::default);
    println!("After resize_with(8, Default::default): {:?}", vec);
    
    // Shrink to fit
    let mut vec2: SmallVec<[i32; 2]> = smallvec![1, 2, 3, 4, 5];
    println!("Before shrink: capacity={}", vec2.capacity());
    vec2.shrink_to_fit();
    println!("After shrink: capacity={}", vec2.capacity());
}

Slicing and Splitting

use smallvec::{smallvec, SmallVec};
 
fn main() {
    let vec: SmallVec<[i32; 4]> = smallvec![1, 2, 3, 4, 5];
    
    // As slice
    let slice: &[i32] = &vec;
    println!("As slice: {:?}", slice);
    
    // As mutable slice
    let mut vec2: SmallVec<[i32; 4]> = smallvec![1, 2, 3];
    {
        let slice_mut: &mut [i32] = &mut vec2;
        slice_mut[0] = 100;
    }
    println!("After mutation via slice: {:?}", vec2);
    
    // Split at
    let (left, right) = vec.split_at(2);
    println!("Left: {:?}", left);
    println!("Right: {:?}", right);
}

Working with SmallVec in Functions

use smallvec::{smallvec, SmallVec};
 
// Accept SmallVec parameter
fn process_numbers(nums: &SmallVec<[i32; 8]>) -> i32 {
    nums.iter().sum()
}
 
// Return SmallVec
fn get_multiples(n: i32, count: usize) -> SmallVec<[i32; 8]> {
    (1..=count as i32).map(|i| n * i).collect()
}
 
// Generic over array size
fn process_any<const N: usize>(vec: &SmallVec<[i32; N]>) -> i32 {
    vec.len() as i32
}
 
fn main() {
    let nums: SmallVec<[i32; 8]> = smallvec![1, 2, 3, 4, 5];
    let sum = process_numbers(&nums);
    println!("Sum: {}", sum);
    
    let multiples = get_multiples(3, 5);
    println!("Multiples of 3: {:?}", multiples);
    
    let vec: SmallVec<[i32; 4]> = smallvec![1, 2, 3];
    let len = process_any(&vec);
    println!("Length: {}", len);
}

Comparison with Vec

use smallvec::{smallvec, SmallVec};
use std::mem::size_of;
 
fn main() {
    // Size comparison
    println!("Size of Vec<i32>: {} bytes", size_of::<Vec<i32>>());
    println!("Size of SmallVec<[i32; 4]>: {} bytes", size_of::<SmallVec<[i32; 4]>>());
    
    // Create both types
    let mut regular: Vec<i32> = Vec::new();
    let mut small: SmallVec<[i32; 4]> = SmallVec::new();
    
    // Add same elements
    for i in 1..=6 {
        regular.push(i);
        small.push(i);
    }
    
    println!("\nVec contents: {:?}", regular);
    println!("SmallVec contents: {:?}", small);
    
    println!("\nVec capacity: {}", regular.capacity());
    println!("SmallVec capacity: {}", small.capacity());
    println!("SmallVec spilled: {}", small.spilled());
}

Performance Benchmarking

use smallvec::{smallvec, SmallVec};
use std::time::Instant;
 
fn bench_vec(n: usize) -> u64 {
    let start = Instant::now();
    for _ in 0..n {
        let mut v = Vec::new();
        v.push(1i32);
        v.push(2);
        v.push(3);
        std::hint::black_box(v);
    }
    start.elapsed().as_nanos() as u64
}
 
fn bench_smallvec(n: usize) -> u64 {
    let start = Instant::now();
    for _ in 0..n {
        let mut v: SmallVec<[i32; 4]> = SmallVec::new();
        v.push(1);
        v.push(2);
        v.push(3);
        std::hint::black_box(v);
    }
    start.elapsed().as_nanos() as u64
}
 
fn main() {
    let iterations = 1_000_000;
    
    let vec_time = bench_vec(iterations);
    let small_time = bench_smallvec(iterations);
    
    println!("Vec time: {} ns", vec_time);
    println!("SmallVec time: {} ns", small_time);
    println!("Speedup: {:.2}x", vec_time as f64 / small_time as f64);
}

Nested SmallVecs

use smallvec::{smallvec, SmallVec};
 
type Row = SmallVec<[i32; 4]>;
type Matrix = SmallVec<[Row; 4]>;
 
fn main() {
    let mut matrix: Matrix = SmallVec::new();
    
    // Add rows (each with inline storage)
    matrix.push(smallvec![1, 2, 3]);
    matrix.push(smallvec![4, 5, 6]);
    matrix.push(smallvec![7, 8, 9]);
    
    println!("Matrix:");
    for row in &matrix {
        println!("  {:?}", row);
    }
    
    // Access element
    let val = matrix[1][2];
    println!("\nmatrix[1][2] = {}", val);
}

Real-World Example: Tokenizer

use smallvec::{smallvec, SmallVec};
 
#[derive(Debug, Clone)]
enum Token {
    Number(i32),
    Operator(char),
    Identifier(String),
}
 
fn tokenize(input: &str) -> SmallVec<[Token; 8]> {
    let mut tokens = SmallVec::new();
    
    for word in input.split_whitespace() {
        if let Ok(n) = word.parse::<i32>() {
            tokens.push(Token::Number(n));
        } else if ["+", "-", "*", "/"].contains(&word) {
            tokens.push(Token::Operator(word.chars().next().unwrap()));
        } else {
            tokens.push(Token::Identifier(word.to_string()));
        }
    }
    
    tokens
}
 
fn main() {
    let input = "let x = 10 + 20 * y";
    let tokens = tokenize(input);
    
    println!("Tokens: {:?}", tokens);
    println!("Inline: {}", !tokens.spilled());
}

Real-World Example: Path Segments

use smallvec::{smallvec, SmallVec};
 
fn split_path(path: &str) -> SmallVec<[&str; 8]> {
    path.split('/')
        .filter(|s| !s.is_empty())
        .collect()
}
 
fn join_path(segments: &SmallVec<[&str; 8]>) -> String {
    segments.join("/")
}
 
fn main() {
    let path = "/usr/local/bin/rustc";
    let segments = split_path(path);
    
    println!("Segments: {:?}", segments);
    println!("Count: {}", segments.len());
    println!("Inline: {}", !segments.spilled());
    
    let rejoined = join_path(&segments);
    println!("Rejoined: {}", rejoined);
}

Real-World Example: Small String Buffer

use smallvec::SmallVec;
 
fn process_text(text: &str) -> SmallVec<[char; 32]> {
    // Most words are < 32 chars, so inline storage
    text.chars().collect()
}
 
fn reverse_chars(chars: &mut SmallVec<[char; 32]>) {
    chars.reverse();
}
 
fn main() {
    let text = "Hello, World!";
    let mut chars = process_text(text);
    
    println!("Original: {:?}", chars.iter().collect::<String>());
    println!("Inline: {}", !chars.spilled());
    
    reverse_chars(&mut chars);
    println!("Reversed: {:?}", chars.iter().collect::<String>());
}

Real-World Example: Event Queue

use smallvec::{smallvec, SmallVec};
 
#[derive(Debug)]
enum Event {
    Click { x: i32, y: i32 },
    KeyPress(char),
    Scroll(i32),
}
 
struct EventHandler {
    // Most frames have < 16 events
    pending: SmallVec<[Event; 16]>,
}
 
impl EventHandler {
    fn new() -> Self {
        Self {
            pending: SmallVec::new(),
        }
    }
    
    fn push(&mut self, event: Event) {
        self.pending.push(event);
    }
    
    fn process_all(&mut self) {
        for event in self.pending.drain(..) {
            match event {
                Event::Click { x, y } => println!("Click at ({}, {})", x, y),
                Event::KeyPress(c) => println!("Key: {}", c),
                Event::Scroll(delta) => println!("Scroll: {}", delta),
            }
        }
    }
}
 
fn main() {
    let mut handler = EventHandler::new();
    
    // Add events
    handler.push(Event::Click { x: 100, y: 200 });
    handler.push(Event::KeyPress('a'));
    handler.push(Event::Scroll(5));
    
    println!("Pending: {}", handler.pending.len());
    println!("Inline: {}", !handler.pending.spilled());
    
    handler.process_all();
}

Choosing the Right Inline Capacity

use smallvec::{smallvec, SmallVec};
use std::mem::size_of;
 
fn main() {
    // Trade-off: larger inline capacity = more stack space
    
    // Too small: frequent heap allocation
    let tiny: SmallVec<[i32; 1]> = smallvec![1, 2]; // Spilled!
    println!("Tiny spilled: {}", tiny.spilled());
    
    // Good size: covers common case
    let good: SmallVec<[i32; 4]> = smallvec![1, 2, 3]; // Inline!
    println!("Good spilled: {}", good.spilled());
    
    // Too large: wastes stack space
    println!("Size of SmallVec<[i32; 4]>: {} bytes", size_of::<SmallVec<[i32; 4]>>());
    println!("Size of SmallVec<[i32; 64]>: {} bytes", size_of::<SmallVec<[i32; 64]>>());
    
    // Guidelines:
    // 1. Analyze your data distribution
    // 2. Choose capacity covering 80-90% of cases
    // 3. Consider element size (larger elements = smaller capacity)
}

Interoperability with APIs

use smallvec::{smallvec, SmallVec};
 
// Convert to Vec for APIs that require it
fn api_requires_vec(items: Vec<i32>) -> i32 {
    items.into_iter().sum()
}
 
// Accept slice for flexibility
fn api_accepts_slice(items: &[i32]) -> i32 {
    items.iter().sum()
}
 
fn main() {
    let small: SmallVec<[i32; 4]> = smallvec![1, 2, 3, 4, 5];
    
    // Use as slice (no allocation)
    let sum1 = api_accepts_slice(&small);
    println!("Sum via slice: {}", sum1);
    
    // Convert to Vec when needed
    let sum2 = api_requires_vec(small.into_vec());
    println!("Sum via Vec: {}", sum2);
}

Memory Layout Comparison

use smallvec::{smallvec, SmallVec};
use std::mem::{size_of, align_of};
 
fn main() {
    println!("=== Memory Layout ===\n");
    
    println!("Vec<i32>:");
    println!("  Size: {} bytes", size_of::<Vec<i32>>());
    println!("  Align: {} bytes", align_of::<Vec<i32>>());
    
    println!("\nSmallVec<[i32; 0]> (heap only):" );
    println!("  Size: {} bytes", size_of::<SmallVec<[i32; 0]>>());
    
    println!("\nSmallVec<[i32; 4]>:" );
    println!("  Size: {} bytes", size_of::<SmallVec<[i32; 4]>>());
    
    println!("\nSmallVec<[i32; 8]>:" );
    println!("  Size: {} bytes", size_of::<SmallVec<[i32; 8]>>());
    
    println!("\nSmallVec<[String; 2]>:" );
    println!("  Size: {} bytes", size_of::<SmallVec<[String; 2]>>());
}

Summary

  • SmallVec<[T; N]> stores up to N elements inline (on stack)
  • spilled() returns true when data is on the heap
  • smallvec![] macro creates SmallVecs with initial values
  • Works as a drop-in replacement for Vec in many cases
  • Automatically spills to heap when capacity is exceeded
  • Use into_vec() to convert to regular Vec when needed
  • from_vec() creates SmallVec from Vec
  • Choose inline capacity based on typical usage patterns (80-90% coverage)
  • Larger inline capacity = more stack usage, fewer heap allocations
  • Deref to [T] allows slice operations
  • Perfect for: small collections, avoiding heap allocation, performance-critical code, recursive data structures, parsers/lexers, game development