1045-small-vec — Small Vector Optimization
Tutorial
The Problem
Heap allocation is expensive: a small Vec<T> allocates a buffer on the heap even for zero or one elements. The small vector optimization (SVO) stores elements on the stack up to a threshold, spilling to the heap only when the count exceeds N. LLVM's SmallVector, C++'s llvm::SmallVector, and Rust's smallvec crate all implement this pattern.
For code paths that create many short-lived collections with typically 0–4 elements (AST node children, function argument lists, instruction operands), SVO can eliminate the vast majority of heap allocations.
🎯 Learning Outcomes
SmallVec<T, const N: usize> enum in safe RustMaybeUninit for production implementationssmallvec crate for production useCode Example
#![allow(clippy::all)]
// 1045: Small Vector Optimization Concept
// Stack up to N elements, heap beyond — like SmallVec (concept, std only)
/// SmallVec: stores up to N elements on the stack, spills to heap
enum SmallVec<T, const N: usize> {
Inline {
data: [Option<T>; N], // Using Option since we can't use MaybeUninit safely
len: usize,
},
Heap(Vec<T>),
}
impl<T: Clone + Default, const N: usize> SmallVec<T, N> {
fn new() -> Self {
SmallVec::Inline {
data: std::array::from_fn(|_| None),
len: 0,
}
}
fn push(&mut self, value: T) {
match self {
SmallVec::Inline { data, len } if *len < N => {
data[*len] = Some(value);
*len += 1;
}
SmallVec::Inline { data, len } => {
// Spill to heap
let mut vec = Vec::with_capacity(*len + 1);
for item in data.iter_mut().take(*len) {
if let Some(val) = item.take() {
vec.push(val);
}
}
vec.push(value);
*self = SmallVec::Heap(vec);
}
SmallVec::Heap(vec) => {
vec.push(value);
}
}
}
fn len(&self) -> usize {
match self {
SmallVec::Inline { len, .. } => *len,
SmallVec::Heap(vec) => vec.len(),
}
}
fn is_inline(&self) -> bool {
matches!(self, SmallVec::Inline { .. })
}
fn get(&self, index: usize) -> Option<&T> {
match self {
SmallVec::Inline { data, len } => {
if index < *len {
data[index].as_ref()
} else {
None
}
}
SmallVec::Heap(vec) => vec.get(index),
}
}
fn to_vec(&self) -> Vec<T> {
match self {
SmallVec::Inline { data, len } => data
.iter()
.take(*len)
.filter_map(|x| x.as_ref().cloned())
.collect(),
SmallVec::Heap(vec) => vec.clone(),
}
}
}
fn basic_small_vec() {
let mut sv: SmallVec<i32, 4> = SmallVec::new();
sv.push(1);
sv.push(2);
sv.push(3);
assert_eq!(sv.len(), 3);
assert!(sv.is_inline()); // Still on stack
assert_eq!(sv.to_vec(), vec![1, 2, 3]);
sv.push(4); // At capacity, still inline
assert!(sv.is_inline());
sv.push(5); // Spills to heap
assert!(!sv.is_inline());
assert_eq!(sv.to_vec(), vec![1, 2, 3, 4, 5]);
}
fn indexed_access() {
let mut sv: SmallVec<&str, 3> = SmallVec::new();
sv.push("hello");
sv.push("world");
assert_eq!(sv.get(0), Some(&"hello"));
assert_eq!(sv.get(1), Some(&"world"));
assert_eq!(sv.get(2), None);
}
fn spill_behavior() {
let mut sv: SmallVec<i32, 2> = SmallVec::new();
sv.push(10);
sv.push(20);
assert!(sv.is_inline());
sv.push(30); // Spills
assert!(!sv.is_inline());
assert_eq!(sv.len(), 3);
assert_eq!(sv.get(2), Some(&30));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic() {
basic_small_vec();
}
#[test]
fn test_indexed() {
indexed_access();
}
#[test]
fn test_spill() {
spill_behavior();
}
#[test]
fn test_empty() {
let sv: SmallVec<i32, 4> = SmallVec::new();
assert_eq!(sv.len(), 0);
assert!(sv.is_inline());
assert_eq!(sv.get(0), None);
}
#[test]
fn test_large_n() {
let mut sv: SmallVec<i32, 16> = SmallVec::new();
for i in 0..16 {
sv.push(i);
}
assert!(sv.is_inline());
sv.push(16);
assert!(!sv.is_inline());
}
}Key Differences
const generics**: Rust uses const N: usize for the stack capacity as a type parameter; OCaml would need a functor parameter.smallvec, arrayvec, and tinyvec are widely used in Rust; OCaml equivalents are rare.OCaml Approach
OCaml has no direct equivalent because the GC eliminates heap allocation overhead — small allocations are handled by the minor heap and collected quickly. The optimization is not needed:
(* OCaml: always heap-allocated, GC handles efficiently *)
let small_collection = [1; 2; 3] (* minor heap allocation, fast GC *)
For truly performance-critical OCaml code, Bytes or Bigarray provide stack-like storage without GC overhead, but the pattern is rare.
Full Source
#![allow(clippy::all)]
// 1045: Small Vector Optimization Concept
// Stack up to N elements, heap beyond — like SmallVec (concept, std only)
/// SmallVec: stores up to N elements on the stack, spills to heap
enum SmallVec<T, const N: usize> {
Inline {
data: [Option<T>; N], // Using Option since we can't use MaybeUninit safely
len: usize,
},
Heap(Vec<T>),
}
impl<T: Clone + Default, const N: usize> SmallVec<T, N> {
fn new() -> Self {
SmallVec::Inline {
data: std::array::from_fn(|_| None),
len: 0,
}
}
fn push(&mut self, value: T) {
match self {
SmallVec::Inline { data, len } if *len < N => {
data[*len] = Some(value);
*len += 1;
}
SmallVec::Inline { data, len } => {
// Spill to heap
let mut vec = Vec::with_capacity(*len + 1);
for item in data.iter_mut().take(*len) {
if let Some(val) = item.take() {
vec.push(val);
}
}
vec.push(value);
*self = SmallVec::Heap(vec);
}
SmallVec::Heap(vec) => {
vec.push(value);
}
}
}
fn len(&self) -> usize {
match self {
SmallVec::Inline { len, .. } => *len,
SmallVec::Heap(vec) => vec.len(),
}
}
fn is_inline(&self) -> bool {
matches!(self, SmallVec::Inline { .. })
}
fn get(&self, index: usize) -> Option<&T> {
match self {
SmallVec::Inline { data, len } => {
if index < *len {
data[index].as_ref()
} else {
None
}
}
SmallVec::Heap(vec) => vec.get(index),
}
}
fn to_vec(&self) -> Vec<T> {
match self {
SmallVec::Inline { data, len } => data
.iter()
.take(*len)
.filter_map(|x| x.as_ref().cloned())
.collect(),
SmallVec::Heap(vec) => vec.clone(),
}
}
}
fn basic_small_vec() {
let mut sv: SmallVec<i32, 4> = SmallVec::new();
sv.push(1);
sv.push(2);
sv.push(3);
assert_eq!(sv.len(), 3);
assert!(sv.is_inline()); // Still on stack
assert_eq!(sv.to_vec(), vec![1, 2, 3]);
sv.push(4); // At capacity, still inline
assert!(sv.is_inline());
sv.push(5); // Spills to heap
assert!(!sv.is_inline());
assert_eq!(sv.to_vec(), vec![1, 2, 3, 4, 5]);
}
fn indexed_access() {
let mut sv: SmallVec<&str, 3> = SmallVec::new();
sv.push("hello");
sv.push("world");
assert_eq!(sv.get(0), Some(&"hello"));
assert_eq!(sv.get(1), Some(&"world"));
assert_eq!(sv.get(2), None);
}
fn spill_behavior() {
let mut sv: SmallVec<i32, 2> = SmallVec::new();
sv.push(10);
sv.push(20);
assert!(sv.is_inline());
sv.push(30); // Spills
assert!(!sv.is_inline());
assert_eq!(sv.len(), 3);
assert_eq!(sv.get(2), Some(&30));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic() {
basic_small_vec();
}
#[test]
fn test_indexed() {
indexed_access();
}
#[test]
fn test_spill() {
spill_behavior();
}
#[test]
fn test_empty() {
let sv: SmallVec<i32, 4> = SmallVec::new();
assert_eq!(sv.len(), 0);
assert!(sv.is_inline());
assert_eq!(sv.get(0), None);
}
#[test]
fn test_large_n() {
let mut sv: SmallVec<i32, 16> = SmallVec::new();
for i in 0..16 {
sv.push(i);
}
assert!(sv.is_inline());
sv.push(16);
assert!(!sv.is_inline());
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic() {
basic_small_vec();
}
#[test]
fn test_indexed() {
indexed_access();
}
#[test]
fn test_spill() {
spill_behavior();
}
#[test]
fn test_empty() {
let sv: SmallVec<i32, 4> = SmallVec::new();
assert_eq!(sv.len(), 0);
assert!(sv.is_inline());
assert_eq!(sv.get(0), None);
}
#[test]
fn test_large_n() {
let mut sv: SmallVec<i32, 16> = SmallVec::new();
for i in 0..16 {
sv.push(i);
}
assert!(sv.is_inline());
sv.push(16);
assert!(!sv.is_inline());
}
}
Deep Comparison
Small Vector Optimization — Comparison
Core Insight
The small-vec optimization stores elements inline (on the stack) up to a fixed capacity N, then spills to the heap. This matters in Rust/C++ where heap allocation has measurable cost. In OCaml, the GC's minor heap makes small allocations nearly free, so this optimization is unnecessary.
OCaml Approach
Inline of array * int | Heap of listRust Approach
SmallVec<T, const N: usize>Inline { data: [Option<T>; N], len } for stack storageHeap(Vec<T>) after spillsmallvec or tinyvec crateComparison Table
| Feature | OCaml | Rust |
|---|---|---|
| Heap alloc cost | ~1ns (GC bump) | ~20-50ns (system allocator) |
| Optimization needed | No | Yes, for hot paths |
| Stack storage | N/A (GC managed) | [Option<T>; N] or MaybeUninit |
| Spill point | N/A | Configurable via const generic |
| Real-world impl | Not used | smallvec, tinyvec crates |
| Key benefit | None (GC is fast) | Avoid allocator + better locality |
Exercises
arrayvec::ArrayVec<T, N> (from the arrayvec crate) to avoid the Option<T> overhead.iter(&self) -> impl Iterator<Item=&T> for SmallVec that works transparently for both inline and heap storage.SmallVec<i32, 4> vs Vec<i32> for collections of 1–10 elements using criterion to measure the SVO benefit.