"Small vector" optimization for Rust: store up to a small number of items on the stack
  • Rust 99.1%
  • Shell 0.9%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-05-11 10:10:02 +02:00
benches Rework specialization (#384) 2025-07-06 22:36:47 +00:00
fuzz chore: sync dependencies (monorepo) 2026-04-06 15:20:39 +02:00
scripts Improved const evaluation for ZST checks. (#401) 2026-02-16 04:06:32 +00:00
src chore: sync dependencies (monorepo) 2026-04-06 20:09:07 +02:00
tests feat: implement rfc240 2022-08-01 17:36:10 +02:00
.gitignore Port feature drain_filter (#292) to v2 (#333) 2024-01-15 02:14:55 +00:00
Cargo.toml Fix dependencies to use git repositories instead of local paths 2026-05-11 10:10:02 +02:00
LICENSE-APACHE Relicense as MIT/Apache-2.0 2018-06-06 09:19:13 -07:00
LICENSE-MIT Relicense as MIT/Apache-2.0 2018-06-06 09:19:13 -07:00
README.md chore: sync dependencies (monorepo) 2026-04-07 18:06:18 +02:00

SmallVec

⚠️ Note: This is smallvec 2.0.0-alpha.12, a complete rewrite using const generics. For the stable 1.x version, see the v1 branch.

SmallVec is a vector implementation that stores elements inline on the stack when the count is small, automatically spilling to the heap only when necessary. This provides cache-locality benefits and reduces allocator traffic for typical workloads.

Table of Contents

Overview

SmallVec uses a union-based storage strategy with a tagged length field to seamlessly transition between stack and heap storage:

flowchart TB
    classDef stack fill:#d4edda,stroke:#28a745,stroke-width:2px
    classDef heap fill:#d1ecf1,stroke:#17a2b8,stroke-width:2px
    classDef meta fill:#fff3cd,stroke:#ffc107,stroke-width:2px
    classDef encoding fill:#f8d7da,stroke:#dc3545,stroke-width:2px
    
    subgraph SV["SmallVec<T, N>"]
        direction TB
        L["len: TaggedLen<T>"]
        R["raw: RawSmallVec<T, N>"]
        M["_marker: PhantomData<T>"]
    end
    
    subgraph RL["RawSmallVec Union"]
        direction TB
        I["inline: ManuallyDrop<MaybeUninit<[T; N]>>"]
        H["heap: (NonNull<T>, usize)"]
    end
    
    subgraph TL["TaggedLen Encoding"]
        direction TB
        B["Bit 0: on_heap flag"]
        V["Bits 1..: length << 1"]
    end
    
    R --> I
    R --> H
    L --> B
    L --> V
    
    class I,M stack
    class H heap
    class L,R meta
    class B,V encoding

Memory Layout Visualization

flowchart TB
    classDef stack fill:#d4edda,stroke:#28a745,stroke-width:2px
    classDef heap fill:#d1ecf1,stroke:#17a2b8,stroke-width:2px
    classDef meta fill:#fff3cd,stroke:#ffc107,stroke-width:2px
    classDef union fill:#e2e3e5,stroke:#6c757d,stroke-width:2px
    
    subgraph InlineState["INLINE State (len ≤ N)"]
        direction TB
        IL["len: usize (on_heap=false)"]
        
        subgraph InlineUnion["RawSmallVec"]
            II["inline: [T; N] as MaybeUninit"]
        end
        
        IM["_marker: PhantomData<T>"]
    end
    
    subgraph HeapState["HEAP State (len > N)"]
        direction TB
        HL["len: usize (on_heap=true)"]
        
        subgraph HeapUnion["RawSmallVec"]
            HH1["heap.ptr: NonNull<T>"]
            HH2["heap.cap: usize"]
        end
        
        HM["_marker: PhantomData<T>"]
    end
    
    subgraph HeapAlloc["Heap Allocation"]
        direction TB
        HE1["Element 0"]
        HE2["Element 1"]
        HE3["..."]
        HE4["Element len-1"]
    end
    
    HH1 -.->|points to| HeapAlloc
    
    class II,IL,IM stack
    class HH1,HH2,HL,HM heap
    class InlineUnion,HeapUnion union

Quick Start

Add to your Cargo.toml:

[dependencies]
smallvec = "2.0.0-alpha.12"

Basic usage:

use smallvec::{SmallVec, smallvec};

// Create a SmallVec with inline capacity for 4 items
let mut v: SmallVec<i32, 4> = smallvec![1, 2, 3, 4];

// Access elements like a regular slice
assert_eq!(v[0], 1);
v[0] = 10;

// Push elements - stays inline if ≤ 4 elements
v.push(5); // Now spilled to heap!

// Automatic conversion back to Vec
let vec: Vec<i32> = v.into_vec();

Architecture

State Transitions

stateDiagram-v2
    [*] --> Empty
    Empty --> Inline : push() while len < N
    Inline --> Inline : push() while len < N
    Inline --> Spilled : push() when len == N
    Spilled --> Spilled : push() / grow()
    Spilled --> Inline : shrink_to_fit()<br/>when len ≤ N
    
    Empty --> [*] : drop
    Inline --> [*] : drop elements
    Spilled --> [*] : drop elements + dealloc
    
    note right of Inline
        Stack storage
        No allocation
        Better cache locality
    end note
    
    note right of Spilled
        Heap storage
        Dynamic capacity
        Standard Vec-like
    end note

Method Categories

mindmap
  root((SmallVec API))
    Construction
      ["new()"]
      ["with_capacity()"]
      ["from_buf()"]
      ["from_vec()"]
      ["smallvec![]"]
      ["smallvec_inline![]"]
    ElementAccess["Element Access"]
      ["Indexing []"]
      ["get()"]
      ["first() / last()"]
      ["as_slice()"]
      ["as_mut_slice()"]
    Mutation
      ["push()"]
      ["pop()"]
      ["insert()"]
      ["remove()"]
      ["swap_remove()"]
    BulkOperations["Bulk Operations"]
      ["extend()"]
      ["append()"]
      ["split_off()"]
      ["drain()"]
      ["splice()"]
      ["clear()"]
    Capacity
      ["capacity()"]
      ["reserve()"]
      ["shrink_to_fit()"]
      ["grow()"]
      ["spilled()"]
    Conversion
      ["into_vec()"]
      ["into_inner()"]
      ["into_boxed_slice()"]
      ["from_raw_parts()"]
    Filtering
      ["retain()"]
      ["dedup()"]
      ["dedup_by()"]
      ["extract_if (unstable)"]

API Reference

Construction

SmallVec::new()

Creates an empty SmallVec with const-generic inline capacity.

use smallvec::SmallVec;

let v: SmallVec<i32, 8> = SmallVec::new();
assert!(v.is_empty());
assert!(!v.spilled());
assert_eq!(v.capacity(), 8);

SmallVec::with_capacity()

Creates with at least the specified capacity. May allocate on heap if capacity > N.

use smallvec::SmallVec;

// Fits inline
let v: SmallVec<u8, 4> = SmallVec::with_capacity(2);
assert!(!v.spilled());
assert_eq!(v.capacity(), 4);

// Requires heap
let v: SmallVec<u8, 4> = SmallVec::with_capacity(100);
assert!(v.spilled());
assert_eq!(v.capacity(), 100);

SmallVec::from_buf()

Creates from a fixed-size array. The array must fit within inline capacity.

use smallvec::SmallVec;

// Exact fit
let v = SmallVec::<i32, 4>::from_buf([1, 2, 3, 4]);
assert_eq!(v.len(), 4);
assert!(!v.spilled());

// Smaller than capacity
let v = SmallVec::<i32, 8>::from_buf([1, 2]);
assert_eq!(v.len(), 2);
assert!(!v.spilled());

SmallVec::from_buf_and_len()

Creates from buffer with explicit length (drops uninitialized elements).

use smallvec::SmallVec;

let buf = [1, 2, 3, 4, 0, 0, 0, 0];
let v = SmallVec::<i32, 8>::from_buf_and_len(buf, 4);
assert_eq!(&*v, &[1, 2, 3, 4]);

SmallVec::from_vec()

Consumes a Vec, reusing its heap allocation if possible.

use smallvec::SmallVec;

// Small vec - may convert to inline
let vec = vec![1, 2, 3];
let sv: SmallVec<i32, 8> = SmallVec::from_vec(vec);

// Large vec - stays on heap
let vec = vec![1; 1000];
let sv: SmallVec<i32, 8> = SmallVec::from_vec(vec);
assert!(sv.spilled());

Macros

smallvec![]

Vec-like macro with automatic optimization.

use smallvec::{smallvec, SmallVec};

// Empty
let v: SmallVec<i32, 4> = smallvec![];

// From elements
let v = smallvec![1, 2, 3];
assert_eq!(v.len(), 3);

// From element + count (clone)
let v = smallvec![0; 100];
assert_eq!(v.len(), 100);

// Large literal goes directly to Vec
let v: SmallVec<i32, 2> = smallvec![1, 2, 3, 4, 5];
assert!(v.spilled()); // Macro detected overflow

smallvec_inline![]

Guaranteed inline storage macro.

use smallvec::smallvec_inline;

// Const-friendly - evaluated at compile time
const V: smallvec::SmallVec<i32, 2> = {
    use smallvec::SmallVec;
    smallvec_inline![1, 2]
};

// Repeat pattern
let v = smallvec_inline![42; 4]; // [42, 42, 42, 42]

Element Operations

push(), pop(), pop_if()

use smallvec::SmallVec;

let mut v = SmallVec::<i32, 2>::new();

// Push until spill
v.push(1);
v.push(2);
assert!(!v.spilled());
v.push(3); // Spills to heap
assert!(v.spilled());

// Pop
assert_eq!(v.pop(), Some(3));

// Conditional pop
v.push(100);
let big = v.pop_if(|x| *x > 50);
assert_eq!(big, Some(100));

insert(), remove(), swap_remove()

use smallvec::SmallVec;

let mut v = SmallVec::<i32, 8>::from([1, 2, 4, 5]);

// Insert shifts elements
v.insert(2, 3);
assert_eq!(&*v, &[1, 2, 3, 4, 5]);

// Remove shifts elements back
let removed = v.remove(2);
assert_eq!(removed, 3);
assert_eq!(&*v, &[1, 2, 4, 5]);

// swap_remove is O(1) - swaps with last
let swapped = v.swap_remove(1); // swaps 2 with 5
assert_eq!(swapped, 2);
assert_eq!(&*v, &[1, 5, 4]);

Bulk Operations

extend(), append()

use smallvec::SmallVec;

let mut v = SmallVec::<i32, 4>::new();
v.extend([1, 2, 3]);

let mut other = SmallVec::<i32, 2>::from([4, 5]);
v.append(&mut other);

assert_eq!(&*v, &[1, 2, 3, 4, 5]);
assert!(other.is_empty());

split_off()

use smallvec::SmallVec;

let mut v = SmallVec::<i32, 4>::from([1, 2, 3, 4, 5, 6]);
let v2 = v.split_off(3);

assert_eq!(&*v, &[1, 2, 3]);
assert_eq!(&*v2, &[4, 5, 6]);

drain() and splice()

use smallvec::{smallvec, SmallVec};

// Drain removes and yields elements
let mut v: SmallVec<i32, 4> = smallvec![1, 2, 3, 4, 5];
let drained: SmallVec<i32, 4> = v.drain(1..4).collect();
assert_eq!(&*drained, &[2, 3, 4]);
assert_eq!(&*v, &[1, 5]);

// Splice replaces a range
let mut v: SmallVec<i32, 4> = smallvec![1, 2, 3, 4, 5];
let removed: SmallVec<i32, 4> = v.splice(1..4, [10, 20]).collect();
assert_eq!(&*removed, &[2, 3, 4]);
assert_eq!(&*v, &[1, 10, 20, 5]);

Filtering and Deduplication

retain() and extract_if

use smallvec::SmallVec;

// retain keeps elements matching predicate
let mut v = SmallVec::<i32, 8>::from([1, 2, 3, 4, 5]);
v.retain(|&x| x % 2 == 0);
assert_eq!(&*v, &[2, 4]);

// extract_if (requires feature) yields removed elements
#[cfg(feature = "extract_if")]
{
    let mut v = SmallVec::<i32, 8>::from([1, 2, 3, 4, 5, 6]);
    let evens: SmallVec<i32, 8> = v.extract_if(.., |x| *x % 2 == 0).collect();
    assert_eq!(&*evens, &[2, 4, 6]);
    assert_eq!(&*v, &[1, 3, 5]);
}

dedup() variants

use smallvec::SmallVec;

// Simple dedup
let mut v = SmallVec::<i32, 8>::from([1, 1, 2, 2, 3, 3, 3]);
v.dedup();
assert_eq!(&*v, &[1, 2, 3]);

// dedup_by_key
let mut v = SmallVec::<(i32, String), 8>::from([
    (1, "a".to_string()),
    (1, "b".to_string()),
    (2, "c".to_string()),
]);
v.dedup_by_key(|x| x.0);
assert_eq!(v.len(), 2);

// dedup_by with custom comparison
let mut v = SmallVec::<i32, 8>::from([1, 2, 3, 4, 5]);
v.dedup_by(|a, b| (a - b).abs() <= 1);
// Keeps 1, removes 2 (|2-1|<=1), keeps 3, removes 4 (|4-3|<=1), keeps 5
assert_eq!(&*v, &[1, 3, 5]);

Capacity Management

reserve(), shrink_to_fit(), grow()

use smallvec::SmallVec;

// Reserve space
let mut v = SmallVec::<i32, 4>::new();
v.reserve(100); // Forces heap allocation
assert!(v.spilled());

// Shrink back to inline if possible
v.extend(0..3);
v.shrink_to_fit();
assert!(!v.spilled()); // Moved back to stack!

// Manual grow
let mut v = SmallVec::<i32, 2>::new();
v.grow(10); // Explicitly set capacity
assert!(v.spilled());
assert_eq!(v.capacity(), 10);

Conversion

into_vec(), into_inner(), into_boxed_slice()

use smallvec::SmallVec;

// into_vec - converts to Vec
let v = SmallVec::<i32, 4>::from([1, 2, 3]);
let vec: Vec<i32> = v.into_vec();
assert_eq!(vec, vec![1, 2, 3]);

// into_inner - get array if exactly right size
let v = SmallVec::<i32, 3>::from([1, 2, 3]);
let arr: [i32; 3] = v.into_inner().unwrap();
assert_eq!(arr, [1, 2, 3]);

// into_inner returns Err if wrong size
let v = SmallVec::<i32, 3>::from([1, 2]);
let result = v.into_inner();
assert!(result.is_err());

// into_boxed_slice
let v = SmallVec::<i32, 4>::from([1, 2, 3]);
let boxed: Box<[i32]> = v.into_boxed_slice();

Advanced: from_raw_parts

use smallvec::{smallvec, SmallVec};

let mut v: SmallVec<i32, 1> = smallvec![1, 2, 3];

// Extract raw parts (only safe for spilled vectors!)
let ptr = v.as_mut_ptr();
let len = v.len();
let cap = v.capacity();
let spilled = v.spilled();

unsafe {
    std::mem::forget(v);
    assert!(spilled);
    
    // Write new data
    std::ptr::write(ptr.add(0), 10);
    std::ptr::write(ptr.add(1), 20);
    std::ptr::write(ptr.add(2), 30);
    
    // Rebuild SmallVec with different inline capacity
    let rebuilt = SmallVec::<i32, 2>::from_raw_parts(ptr, len, cap);
    assert_eq!(&*rebuilt, &[10, 20, 30]);
}

Time Complexity

Operation Inline Heap Notes
push (amortized) O(1) O(1) Inline may trigger spill once
push (worst) O(N) O(N) When spilling/reallocating
pop O(1) O(1)
insert O(N) O(N)
remove O(N) O(N)
swap_remove O(1) O(1)
get/[] O(1) O(1) Inline slightly faster (no indirection)
shrink_to_fit O(N) O(N) May unspill (move to stack)

Performance Characteristics

SmallVec provides two distinct performance profiles depending on whether data fits inline or spills to heap.

Inline Storage: Cache-Hot Performance

When len <= N, SmallVec stores elements directly in the struct's inline buffer on the stack. This provides:

  • Zero allocation cost: No heap allocator calls, deterministic performance
  • Cache locality: Elements live in the same cache line as the struct itself
  • No pointer indirection: Direct indexing via as_ptr_inline():
// From src/lib.rs:209-214
const fn as_ptr_inline(&self) -> *const T {
    // SAFETY: We only get a pointer, no reads
    addr_of!(self.inline) as *const T
}

The inline pointer is computed as the address of the union's inline member. No heap pointer chasing means single-cache-line access patterns that prefetchers handle well.

Heap Storage: Standard Vec Performance

When len > N, SmallVec behaves identically to Vec<T>:

// From src/lib.rs:226-228
const unsafe fn as_ptr_heap(&self) -> *const T {
    unsafe { self.heap.0.as_ptr() }
}

Access requires loading the heap pointer from the union, then dereferencing. This matches Vec's double-indirection pattern with identical cache characteristics.

Allocation Strategy: Growth and Spilling

When an inline vector needs more capacity, try_grow_raw handles the spill:

// From src/lib.rs:242-292 (simplified)
unsafe fn try_grow_raw(&mut self, len: TaggedLen<T>, new_capacity: usize) 
    -> Result<(), CollectionAllocErr> 
{
    let was_on_heap = len.on_heap();
    let ptr = if was_on_heap {
        unsafe { self.as_mut_ptr_heap() }
    } else {
        self.as_mut_ptr_inline()  // Copy from stack to heap
    };
    
    // Fresh allocation or realloc
    let new_ptr = if len == 0 || !was_on_heap {
        unsafe { alloc(new_layout) }  // First spill: copy from inline
    } else {
        unsafe { realloc(ptr as *mut u8, old_layout, new_layout.size()) }
    };
    
    *self = Self::new_heap(new_ptr, new_capacity);
    Ok(())
}

The worst-case O(N) occurs when:

  1. First spill: copying N elements from stack to new heap allocation
  2. Subsequent growth: realloc may copy data if allocator can't extend in-place

The Spill/Unspill Cycle

SmallVec can move data back to inline storage via shrink_to_fit():

// From src/lib.rs:1380-1403 (simplified)
pub fn shrink_to_fit(&mut self) {
    if !self.spilled() { return; }
    let len = self.len();
    
    if len <= Self::inline_size() {
        // Move from heap back to stack
        unsafe {
            let (ptr, capacity) = self.raw.heap;
            copy_nonoverlapping(ptr.as_ptr(), 
                              self.raw.as_mut_ptr_inline(), 
                              len);
            self.set_inline();
            alloc::alloc::dealloc(ptr.cast().as_ptr(), layout);
        }
    }
}

This "unspilling" allows batch-processing patterns where buffers temporarily grow then return to efficient inline storage.

Zero-Sized Type Optimization

ZSTs never allocate because size_of::<T>() == 0:

// From src/lib.rs:147-151
const fn is_zst<T>() -> bool {
    const { size_of::<T>() == 0 }
}

For ZSTs, TaggedLen ignores the heap flag and stores only length. Since no actual bytes are needed, inline_size() returns usize::MAX - effectively infinite capacity without allocation.

Memory Layout Comparison

Both Vec<T> and SmallVec<T, N> use 24 bytes on 64-bit platforms, but SmallVec packs the inline storage directly into its structure through a union-based design.

Standard Vec Layout

A standard Vec<T> contains three fields stored on the stack:

  • ptr: *mut T - Pointer to heap allocation
  • len: usize - Current number of elements
  • cap: usize - Total capacity of the heap allocation

This means even a Vec with 1 element requires a heap allocation (typically 4-8 elements minimum from the allocator).

SmallVec Union-Based Layout

SmallVec<T, N> uses a #[repr(C)] union to overlay inline and heap storage:

// From src/lib.rs:132-136
#[repr(C)]
pub union RawSmallVec<T, const N: usize> {
    inline: ManuallyDrop<MaybeUninit<[T; N]>>,
    heap: (NonNull<T>, usize),
}

When len <= N, the vector stores elements in the inline union member as a MaybeUninit<[T; N]> on the stack. When spilled to heap, it stores (NonNull<T>, usize) in the heap member pointing to the allocation.

TaggedLen: The Storage Discriminator

The key insight is the TaggedLen type which packs both length AND storage location into a single usize:

// From src/lib.rs:302-303, 320-350
#[repr(transparent)]
struct TaggedLen<T>(usize, PhantomData<T>);

impl<T> TaggedLen<T> {
    pub const fn new(len: usize, on_heap: bool) -> Self {
        if Self::IS_ZST {
            Self(len, PhantomData)  // ZSTs never spill
        } else {
            Self((len << 1) | on_heap as usize, PhantomData)
        }
    }
    
    pub const fn on_heap(self) -> bool {
        (self.0 & 1_usize) == 1
    }
    
    pub const fn value(self) -> usize {
        self.0 >> 1
    }
}

Bit 0 indicates heap vs inline (0 = inline, 1 = heap). The remaining bits store length << 1. For ZSTs, the heap flag is always false since they never allocate.

Capacity Query: Runtime Dispatch

The capacity() method checks the heap flag at runtime:

// From src/lib.rs:1041-1049
pub const fn capacity(&self) -> usize {
    if self.len.on_heap() {
        // SAFETY: raw.heap is active
        unsafe { self.raw.heap.1 }
    } else {
        Self::inline_size()
    }
}

When to Choose Which Inline Size

Capacity Use Case Memory Footprint
SmallVec<T, 0> Always heap, minimal overhead Same as Box<[T]>
SmallVec<T, 4> Small collections, single digits 24 bytes + up to 4×sizeof(T)
SmallVec<T, 16> Buffers, token accumulators 24 bytes + up to 16×sizeof(T)
SmallVec<T, 32> AST nodes, function args 24 bytes + up to 32×sizeof(T)
SmallVec<T, 64> Larger working sets 24 bytes + up to 64×sizeof(T)

For zero-sized types (ZSTs), inline_size() returns usize::MAX since they require no actual storage:

// From src/lib.rs:1022-1028
pub const fn inline_size() -> usize {
    if Self::IS_ZST {
        usize::MAX  // ZSTs never spill
    } else {
        N
    }
}

This means SmallVec<(), N> for any N will never allocate - it effectively has infinite inline capacity since all elements are zero-sized.

Feature Flags

Feature Description Status
std Implements std::io::Write for SmallVec<u8, _> Stable
serde Implements Serialize/Deserialize Stable
bytes Implements BufMut from bytes crate Stable
extract_if Adds extract_if method (unstable API) Unstable
specialization Optimized Copy type handling Nightly only
may_dangle Relaxed drop check for references Nightly only
malloc_size_of Memory profiling support Stable

Serde Example

use smallvec::{SmallVec, smallvec};
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize)]
struct Data {
    items: SmallVec<i32, 4>,
}

let data = Data { items: smallvec![1, 2, 3] };
let json = serde_json::to_string(&data).unwrap();
assert_eq!(json, r#"{"items":[1,2,3]}"#);

Write Trait Example

use smallvec::{SmallVec, smallvec};
use std::io::Write;

let mut buf: SmallVec<u8, 32> = smallvec![];
buf.write_all(b"Hello, ").unwrap();
buf.write_all(b"World!").unwrap();
assert_eq!(&buf, b"Hello, World!");

Advanced Usage

Pattern: SmallVec as a Return Type

use smallvec::SmallVec;

// Return small result sets without allocation
fn find_matches(text: &str, pattern: &str) -> SmallVec<(usize, usize), 4> {
    let mut matches = SmallVec::new();
    let mut start = 0;
    
    while let Some(pos) = text[start..].find(pattern) {
        let match_start = start + pos;
        matches.push((match_start, match_start + pattern.len()));
        if matches.len() == 4 {
            break; // Cap at inline capacity
        }
        start = match_start + 1;
    }
    
    matches
}

Pattern: Recursive Data Structures

use smallvec::SmallVec;

// AST node with usually few children
enum Expr {
    Literal(f64),
    Variable(String),
    Call {
        func: String,
        args: SmallVec<Expr, 4>,  // Most functions have few args
    },
}

Pattern: Batch Processing with Unspilling

use smallvec::SmallVec;

fn process_batches() {
    let mut buffer = SmallVec::<i32, 16>::new();
    
    for i in 0..1000 {
        buffer.push(i);
        
        // Process when buffer fills
        if buffer.len() == 16 {
            process(&buffer);
            buffer.clear();
            // After shrink_to_fit, back to inline storage
            buffer.shrink_to_fit();
        }
    }
    
    // Process remainder
    if !buffer.is_empty() {
        process(&buffer);
    }
}

fn process(data: &[i32]) {
    // Process the batch
}

Comparison with Vec

flowchart TD
    subgraph Decision["When to Choose What?"]
        Q1{"Typical size?"}
        
        Q1 -->|"Known small\n≤ 16 items"| S1["SmallVec\nStack storage\nNo alloc overhead"]
        Q1 -->|"Variable /\nLarge"| S2["Vec\nSimpler\nAlways on heap"]
        Q1 -->|"Always 0\nZST pattern"| S3["SmallVec<_, 0>\nAlways heap\nMinimal overhead"]
        
        Q2{"Performance critical?"}
        S2 --> Q2
        Q2 -->|"Yes, cache\nsensitive"| S4["SmallVec with\nright-sized N"]
        Q2 -->|"No, simpler\nis better"| S5["Vec - less\ncomplexity"]
    end
Aspect Vec SmallVec
Stack size 24 bytes Varies (N * sizeof(T) + overhead)
Heap allocations Always Only when len > N
Cache locality Pointer indirection Direct (inline) or indirect (heap)
API compatibility Standard Drop-in replacement
Memory overhead Minimal Same as Vec on heap
Use case General purpose Known-small collections

Safety and Invariants

SmallVec maintains the same safety invariants as Vec:

  1. Length ≤ Capacity: Always true
  2. Initialized elements: Elements at 0..len are always valid
  3. Proper alignment: All allocations respect type alignment
  4. Drop safety: All elements are dropped on destruction

Additional SmallVec-specific invariants:

  1. TaggedLen encoding: The heap flag correctly indicates storage location
  2. Union safety: Only the active union variant is accessed
  3. ZST handling: Zero-sized types never allocate (infinite capacity)

License

This project is licensed under the MIT or Apache-2.0 license, at your option.