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:
- SmallVec<[T; N]> — stores up to N elements inline, spills to heap for more
- SmallVec::new() — creates an empty SmallVec
- smallvec![] — macro for creating SmallVecs with initial values
- push() — adds elements, spilling to heap if needed
- 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()returnstruewhen data is on the heapsmallvec![]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
