What is the purpose of smallvec::SmallVec::into_vec for heap promotion when inline capacity is exceeded?

into_vec converts a SmallVec into a standard Vec by extracting the heap-allocated storage or promoting inline storage to the heap, returning a Vec<T> that owns all elements. When the SmallVec is already using heap storage (inline capacity exceeded), this is essentially a no-cost transfer of ownership. When elements are still stored inline, into_vec allocates a heap buffer and moves elements to it. This method is used when you need to pass data to APIs expecting Vec, when the small-vector optimization is no longer beneficial, or when you want to guarantee contiguous heap storage for operations that require it.

SmallVec Inline vs Heap Storage

use smallvec::SmallVec;
 
fn main() {
    // SmallVec has inline capacity for N elements, avoiding heap allocation
    // when the size is within N
    
    type SmallIntVec = SmallVec<[i32; 4]>;  // Inline storage for 4 elements
    
    // When elements fit in inline capacity:
    let mut small: SmallIntVec = SmallVec::new();
    small.push(1);
    small.push(2);
    small.push(3);
    // Elements stored inline, no heap allocation
    
    println!("Inline: {:?}", small);
    println!("On stack: {}", small.len());
    
    // When inline capacity is exceeded:
    small.push(4);  // Still inline (capacity 4)
    small.push(5);  // NOW spills to heap!
    
    println!("After spill: {:?}", small);
    // Elements now on heap, with capacity > 4
    
    // The inline storage is unused after spilling
    // All data is on the heap
}

SmallVec stores elements inline until capacity is exceeded, then moves everything to heap storage.

Basic into_vec Usage

use smallvec::SmallVec;
 
fn main() {
    let mut small: SmallVec<[i32; 4]> = SmallVec::new();
    small.push(1);
    small.push(2);
    small.push(3);
    
    // into_vec converts SmallVec to Vec
    // Since elements are inline, this allocates and copies to heap
    let vec: Vec<i32> = small.into_vec();
    
    println!("Vec: {:?}", vec);
    // small is now consumed, cannot be used
    
    // With heap-allocated SmallVec:
    let mut large: SmallVec<[i32; 4]> = SmallVec::new();
    for i in 0..10 {
        large.push(i);
    }
    // Already on heap (exceeded capacity 4)
    
    let vec2: Vec<i32> = large.into_vec();
    // This is essentially free - just reusing the heap allocation
    println!("Vec from spilled: {:?}", vec2);
}

into_vec consumes the SmallVec and returns a Vec with all elements.

Performance: Inline to Heap Promotion

use smallvec::SmallVec;
use std::time::Instant;
 
fn main() {
    // CASE 1: Inline elements -> heap allocation required
    
    let inline_start = Instant::now();
    let small: SmallVec<[i32; 64]> = (0..32).collect();  // Fits inline
    let vec1: Vec<i32> = small.into_vec();  // Allocates heap, moves elements
    let inline_duration = inline_start.elapsed();
    
    println!("Inline to Vec: {:?}", inline_duration);
    
    // CASE 2: Already on heap -> reuse allocation
    
    let heap_start = Instant::now();
    let mut large: SmallVec<[i32; 4]> = SmallVec::new();
    for i in 0..1000 {
        large.push(i);
    }  // Already spilled to heap
    
    let vec2: Vec<i32> = large.into_vec();  // Reuses existing heap allocation
    let heap_duration = heap_start.elapsed();
    
    println!("Heap to Vec: {:?}", heap_duration);
    
    // The second case is faster because:
    // 1. No new allocation needed (reuse existing heap buffer)
    // 2. No element copying (the buffer is just transferred)
}

into_vec on an already-spilled SmallVec reuses the heap buffer; inline requires allocation.

Understanding the Memory Layout

use smallvec::SmallVec;
 
fn main() {
    type MySmallVec = SmallVec<[u64; 2]>;
    
    // SmallVec<[T; N]> has:
    // - Inline buffer: N elements on the stack
    // - Pointer/capacity/len for heap when spilled
    
    // Size of SmallVec on stack:
    println!("Size of SmallVec<[u64; 2]>: {} bytes", std::mem::size_of::<MySmallVec>());
    println!("Size of Vec<u64>: {} bytes", std::mem::size_of::<Vec<u64>>());
    
    // SmallVec is larger than Vec on stack because it includes inline storage
    
    let small: MySmallVec = SmallVec::new();
    // Inline capacity: 2 u64s = 16 bytes on stack
    
    let spilled: MySmallVec = {
        let mut v = SmallVec::new();
        v.push(1);
        v.push(2);
        v.push(3);  // Spills to heap
        v
    };
    
    // Both have the same stack size, but:
    // - `small` has data on stack
    // - `spilled` has data on heap
}

SmallVec stack size includes inline buffer; spilled vectors use heap for actual data.

into_vec Ownership Transfer

use smallvec::SmallVec;
 
fn main() {
    // When SmallVec is inline:
    let inline: SmallVec<[String; 4]> = SmallVec::from(vec![
        "a".to_string(),
        "b".to_string(),
    ]);
    
    // into_vec moves each String to the new Vec
    let vec1: Vec<String> = inline.into_vec();
    // No String cloning - just moves
    
    // When SmallVec is on heap:
    let mut spilled: SmallVec<[String; 2]> = SmallVec::new();
    spilled.push("x".to_string());
    spilled.push("y".to_string());
    spilled.push("z".to_string());  // Spilled
    
    // into_vec transfers heap ownership
    let vec2: Vec<String> = spilled.into_vec();
    // Strings stay in place, Vec takes ownership of the heap allocation
}

into_vec moves elements, not clones—ownership is transferred to the returned Vec.

When to Use into_vec

use smallvec::{SmallVec, smallvec};
 
fn main() {
    // SCENARIO 1: Interfacing with APIs expecting Vec
    
    fn api_expecting_vec(data: Vec<i32>) -> i32 {
        data.into_iter().sum()
    }
    
    let mut small: SmallVec<[i32; 8]> = smallvec![1, 2, 3, 4];
    // API needs Vec, so convert
    let result = api_expecting_vec(small.into_vec());
    println!("Sum: {}", result);
    
    // SCENARIO 2: Long-term storage after collection
    
    let mut collected: SmallVec<[String; 4]> = SmallVec::new();
    for word in "hello world from rust".split_whitespace() {
        collected.push(word.to_string());
    }
    
    // For long-term storage, Vec may be preferred:
    // - Simpler type
    // - No inline buffer size needed
    // - Standard drop behavior
    let stored: Vec<String> = collected.into_vec();
    
    // SCENARIO 3: Operations that require contiguous heap storage
    
    // Some FFI or unsafe code may require heap pointers
    let vec_for_ffi: Vec<u8> = smallvec![1u8, 2, 3, 4, 5].into_vec();
    let ptr = vec_for_ffi.as_ptr();  // Guaranteed heap pointer
    // (Vec always uses heap)
    
    // SCENARIO 4: Growing beyond inline capacity
    
    let mut growing: SmallVec<[i32; 4]> = SmallVec::new();
    for i in 0..3 {
        growing.push(i);
    }
    
    // About to add many more elements
    // Convert to Vec for simpler reallocation
    let mut vec: Vec<i32> = growing.into_vec();
    vec.extend(0..1000);  // Vec handles growth naturally
}

Use into_vec when you need Vec for API compatibility or when the small-vector optimization is no longer applicable.

Comparing with Alternative Operations

use smallvec::{SmallVec, smallvec};
 
fn main() {
    let small: SmallVec<[i32; 4]> = smallvec![1, 2, 3];
    
    // into_vec: Consumes SmallVec, returns Vec
    let vec1: Vec<i32> = small.into_vec();
    // small is now invalid (moved)
    
    let small2: SmallVec<[i32; 4]> = smallvec![1, 2, 3];
    
    // to_vec: Clones elements, returns Vec
    let vec2: Vec<i32> = small2.to_vec();
    // small2 is still valid
    println!("After to_vec, small2: {:?}", small2);
    
    // into_boxed_slice: Consumes, returns Box<[T]>
    let small3: SmallVec<[i32; 4]> = smallvec![4, 5, 6];
    let boxed: Box<[i32]> = small3.into_boxed_slice();
    // Cannot resize a boxed slice
    
    // drain: Removes elements, returns iterator
    let mut small4: SmallVec<[i32; 4]> = smallvec![7, 8, 9];
    let drained: Vec<i32> = small4.drain(..).collect();
    // small4 is now empty but valid
    println!("After drain, small4: {:?}", small4);
    
    // Summary:
    // - into_vec: Move elements, consume SmallVec
    // - to_vec: Clone elements, keep SmallVec
    // - into_boxed_slice: Move to boxed slice (cannot grow)
    // - drain: Move elements out, leave SmallVec empty
}

into_vec moves and consumes; to_vec clones and preserves; other methods have different ownership semantics.

Internal Behavior: Inline Case

use smallvec::SmallVec;
 
fn main() {
    // When into_vec is called on inline data:
    
    let small: SmallVec<[i32; 8]> = SmallVec::from_slice(&[1, 2, 3, 4]);
    // Elements are inline (within capacity 8)
    
    // into_vec does:
    // 1. Allocate heap buffer with capacity >= len
    // 2. Move elements from inline buffer to heap buffer
    // 3. Return Vec pointing to heap buffer
    
    // The inline buffer memory is freed with SmallVec's stack frame
    
    // This has a cost: allocation + move
    // But it's only incurred when SmallVec hasn't already spilled
    
    // Demonstration:
    let many: SmallVec<[i32; 2]> = (0..100).collect();
    // Already on heap
    
    // into_vec on spilled SmallVec:
    // 1. Check: data is on heap (spilled)
    // 2. Take ownership of heap buffer
    // 3. Create Vec from that buffer
    // No allocation or copying needed!
}

into_vec allocates when inline but reuses allocation when already spilled.

Working with Different Inline Capacities

use smallvec::SmallVec;
 
fn main() {
    // Different capacities have different trade-offs
    
    // Capacity 0: Always on heap (like Vec but with SmallVec type)
    type AlwaysHeap = SmallVec<[i32; 0]>;
    let always: AlwaysHeap = SmallVec::from_vec(vec![1, 2, 3]);
    let vec1: Vec<i32> = always.into_vec();  // Always reuses allocation
    
    // Capacity 1: Single element inline, otherwise heap
    type OneInline = SmallVec<[i32; 1]>;
    let mut one: OneInline = SmallVec::new();
    one.push(42);  // Inline
    one.push(43);  // Now on heap
    let vec2: Vec<i32> = one.into_vec();  // Reuses heap
    
    // Large capacity: Many elements inline
    type ManyInline = SmallVec<[i32; 1024]>;
    let many: ManyInline = (0..512).collect();  // Still inline
    // into_vec will allocate and move 512 elements
    let vec3: Vec<i32> = many.into_vec();
    
    // Choosing capacity:
    // - If you know max size, use that + small buffer
    // - If typically small (< N) but sometimes large, use appropriate N
    // - If always large, use Vec directly or SmallVec with capacity 0
}

Choose inline capacity based on typical data size; into_vec behavior depends on spill state.

Real-World Use Case: Building Then Converting

use smallvec::{SmallVec, smallvec};
 
// Common pattern: Collect into SmallVec, then convert to Vec
 
fn process_words(text: &str) -> Vec<String> {
    // Most text has few words, so use inline storage
    // But don't limit - spill to heap if needed
    let mut words: SmallVec<[String; 8]> = SmallVec::new();
    
    for word in text.split_whitespace() {
        words.push(word.to_string());
    }
    
    // Convert to Vec for return
    // - If <= 8 words: heap allocation
    // - If > 8 words: reuse existing heap buffer
    words.into_vec()
}
 
fn main() {
    let short = process_words("hello world");
    println!("Short: {:?}", short);
    
    let long = process_words("the quick brown fox jumps over the lazy dog");
    println!("Long: {:?}", long);
}
 
// This pattern optimizes for common case:
// - Most calls have few words -> stack allocation
// - Occasional large input -> automatic heap
// - Return Vec for compatibility

Collect in SmallVec for stack allocation, then into_vec for return value.

Integration with Other Collections

use smallvec::{SmallVec, smallvec};
 
fn main() {
    // Converting to other collection types
    
    let small: SmallVec<[i32; 4]> = smallvec![1, 2, 3, 4, 5];  // Spilled
    
    // Into Vec
    let vec: Vec<i32> = small.into_vec();
    
    // Note: small is consumed, need to recreate
    let small2: SmallVec<[i32; 4]> = smallvec![1, 2, 3, 4, 5];
    
    // Into Box<[T]> via Vec
    let boxed: Box<[i32]> = small2.into_vec().into_boxed_slice();
    
    // Into VecDeque via Vec
    use std::collections::VecDeque;
    let small3: SmallVec<[i32; 4]> = smallvec![1, 2, 3, 4, 5];
    let deque: VecDeque<i32> = small3.into_vec().into();
    
    // From Vec back to SmallVec
    let vec = vec![1, 2, 3, 4, 5];
    let small4: SmallVec<[i32; 8]> = SmallVec::from_vec(vec);
    
    // Or use From impl
    let small5: SmallVec<[i32; 8]> = vec![1, 2, 3].into();
}

into_vec is a bridge between SmallVec and the broader ecosystem of Vec-based APIs.

Memory Layout Visualization

use smallvec::SmallVec;
 
fn main() {
    // SmallVec<[T; N]> memory layout:
    
    // When len <= N (inline):
    // Stack: [inline_buffer: N * size_of::<T>(), len: usize, capacity: usize]
    // Heap: unused
    
    // When len > N (spilled):
    // Stack: [unused_inline_buffer, len: usize, capacity: usize]
    // Heap: [elements...] (capacity may be > N)
    
    type MyVec = SmallVec<[u64; 2]>;
    
    let inline: MyVec = SmallVec::from_slice(&[1, 2]);
    // Stack memory holds: [1, 2, len=2, cap=2]
    // No heap allocation
    
    let spilled: MyVec = SmallVec::from_slice(&[1, 2, 3]);
    // Stack memory holds: [unused, unused, len=3, cap=heap_capacity]
    // Heap holds: [1, 2, 3, ...]
    
    // into_vec on inline:
    // Vec allocates heap buffer, copies [1, 2] to it
    // Stack SmallVec is dropped
    
    // into_vec on spilled:
    // Vec takes ownership of heap buffer [1, 2, 3, ...]
    // No copy needed
}

The spill state determines whether into_vec copies or just transfers ownership.

Synthesis

Behavior summary:

SmallVec State into_vec Behavior Performance
Inline (len <= capacity) Allocate heap, copy elements O(n) allocation + copy
Spilled (len > capacity) Transfer heap ownership O(1), no copy

Memory operations:

// Inline SmallVec -> Vec
// 1. Vec allocates heap buffer (size >= len)
// 2. Elements moved from SmallVec's inline buffer
// 3. SmallVec dropped (inline buffer freed with stack)
 
// Spilled SmallVec -> Vec
// 1. Vec takes ownership of SmallVec's heap buffer
// 2. No allocation or copying
// 3. SmallVec's stack part dropped; heap remains

When to use into_vec:

  • Returning data to APIs expecting Vec
  • Long-term storage after short-term collection
  • When SmallVec's inline buffer size is no longer beneficial
  • Before FFI or operations requiring heap pointers
  • When Vec's simpler type is preferable

When to keep SmallVec:

  • Still within inline capacity
  • Frequent push/pop operations
  • Short-lived data
  • Function boundaries that accept SmallVec

Key insight: into_vec bridges the small-vector optimization and standard Vec. The small-vector optimization excels when data stays inline—stack allocation avoids heap overhead entirely. But once spilled to heap, SmallVec behaves like Vec with extra metadata. into_vec formalizes this transition: when inline, it pays the cost of one heap allocation and move; when already spilled, it's essentially free, just transferring ownership. The common pattern is collecting into SmallVec during a computation (benefiting from inline storage for typical cases), then converting to Vec for storage or API compatibility. This gives you the best of both worlds: stack allocation for small data, seamless transition to heap for large data, and Vec compatibility for the broader ecosystem.