How does smallvec::SmallVec::into_vec differ from into_inner for heap promotion and inline extraction?
into_vec converts a SmallVec into a standard Vec by moving all elements onto the heap regardless of whether they were originally stored inline or on the heap, while into_inner attempts to return the inline array directly if all elements fit, otherwise returning the heap-allocated Vec as an Err variant that must be handled. This distinction matters for performance: into_vec always heap-allocates, while into_inner preserves inline storage when possible, avoiding heap allocation for small vectors.
SmallVec Basics
use smallvec::SmallVec;
// SmallVec stores elements inline up to N elements
// Beyond N, it spills to the heap
type SmallInts = SmallVec<[i32; 4]>; // Inline storage for up to 4 elements
fn smallvec_basics() {
// Inline: 0-4 elements stored on the stack
let mut small: SmallInts = SmallVec::new();
small.push(1);
small.push(2);
small.push(3);
// These are stored inline, no heap allocation
// Spilled: More than 4 elements, heap allocation
small.push(4); // Still inline (4 elements)
small.push(5); // Now spills to heap
// The transition from inline to heap is automatic
}SmallVec uses inline storage for small numbers of elements and heap-allocates for larger collections.
The into_vec Method
use smallvec::SmallVec;
fn into_vec_example() {
type SmallVec4 = SmallVec<[i32; 4]>;
// Case 1: Elements were inline
let inline: SmallVec4 = smallvec::smallvec![1, 2, 3];
let vec: Vec<i32> = inline.into_vec();
// Result: Always a heap-allocated Vec
// Elements are moved from inline storage to the heap
// Case 2: Elements were already on heap
let spilled: SmallVec4 = smallvec::smallvec![1, 2, 3, 4, 5]; // Exceeds inline capacity
let vec: Vec<i32> = spilled.into_vec();
// Result: Vec wrapping existing heap allocation
// No additional allocation needed
// In both cases, result type is Vec<i32>
}into_vec always returns a Vec, converting inline elements to heap storage if necessary.
The into_inner Method
use smallvec::SmallVec;
fn into_inner_example() {
type SmallVec4 = SmallVec<[i32; 4]>;
// Case 1: Elements fit inline
let inline: SmallVec4 = smallvec::smallvec![1, 2, 3];
let result: Result<[i32; 4], Vec<i32>> = inline.into_inner();
match result {
Ok(array) => {
// Elements were inline, returned as array
// No heap allocation occurred
assert_eq!(array, [1, 2, 3, 0]); // Note: includes uninitialized tail
}
Err(vec) => {
// Elements were on heap, returned as Vec
// This branch won't execute for this case
}
}
// Case 2: Elements spilled to heap
let spilled: SmallVec4 = smallvec::smallvec![1, 2, 3, 4, 5];
let result: Result<[i32; 4], Vec<i32>> = spilled.into_inner();
match result {
Ok(array) => {
// Won't execute: elements don't fit inline
}
Err(vec) => {
// Elements were on heap
assert_eq!(vec, vec![1, 2, 3, 4, 5]);
}
}
}into_inner returns Ok(array) if elements fit inline, Err(vec) if spilled to heap.
Key Differences
use smallvec::SmallVec;
fn compare_methods() {
type SmallVec4 = SmallVec<[i32; 4]>;
// into_vec: Always returns Vec
let small: SmallVec4 = smallvec::smallvec![1, 2];
let vec: Vec<i32> = small.into_vec();
// Type: Vec<i32>
// Guarantee: Always heap-allocated
// into_inner: Returns Result<[N], Vec>
let small: SmallVec4 = smallvec::smallvec![1, 2];
let result: Result<[i32; 4], Vec<i32>> = small.into_inner();
// Type: Result<[i32; 4], Vec<i32>>
// Behavior: Ok if inline, Err if spilled
// into_vec is simpler (no Result handling)
// into_inner preserves inline storage when possible
}| Method | Return Type | Heap Allocation | Use Case |
|---|---|---|---|
into_vec |
Vec |
Always (inline → heap) | Simplicity, guaranteed Vec |
into_inner |
Result<[N], Vec> |
Only if spilled | Preserve inline storage |
Memory Layout and Performance
use smallvec::SmallVec;
fn memory_layout() {
type SmallVec8 = SmallVec<[u64; 8]>;
// SmallVec<[T; N]> layout:
// - Inline: array of N elements + metadata (len, capacity on heap)
// - Heap: pointer to heap allocation + len + capacity
// When inline (0-8 elements):
let inline: SmallVec8 = smallvec::smallvec![1, 2, 3, 4];
// Memory: entirely on stack
// Size: sizeof([u64; 8]) + metadata ≈ 8*8 + 16 = 80 bytes
// When spilled (9+ elements):
let spilled: SmallVec8 = smallvec::smallvec![1u64; 10];
// Memory: SmallVec on stack, data on heap
// into_vec on inline:
let vec = inline.into_vec();
// Allocation: heap allocation for elements
// Copy: elements copied from stack to heap
// into_inner on inline:
let small: SmallVec8 = smallvec::smallvec![1, 2, 3, 4];
let result = small.into_inner();
// No allocation: returns array directly
// Elements stay on stack
}into_inner avoids heap allocation when elements are inline; into_vec may allocate.
When Elements Are Already on Heap
use smallvec::SmallVec;
fn already_spilled() {
type SmallVec4 = SmallVec<[i32; 4]>;
let spilled: SmallVec4 = smallvec::smallvec![1, 2, 3, 4, 5]; // 5 > 4
// into_vec: Just unwraps the Vec, no new allocation
let vec: Vec<i32> = spilled.into_vec();
// The Vec already exists on heap, into_vec returns it
let spilled: SmallVec4 = smallvec::smallvec![1, 2, 3, 4, 5];
// into_inner: Returns Err(Vec)
let result = spilled.into_inner();
match result {
Ok(_array) => unreachable!("won't happen"),
Err(vec) => {
// Same as into_vec result
assert_eq!(vec, vec![1, 2, 3, 4, 5]);
}
}
// When already spilled, into_vec and into_inner().unwrap_err()
// are essentially equivalent
}When already spilled, both methods return the existing heap allocation.
The Uninitialized Tail in into_inner
use smallvec::SmallVec;
fn uninitialized_tail() {
type SmallVec4 = SmallVec<[i32; 4]>;
let small: SmallVec4 = smallvec::smallvec![10, 20];
let array: [i32; 4] = small.into_inner().unwrap();
// array contains: [10, 20, ?, ?]
// Elements 0, 1: initialized (10, 20)
// Elements 2, 3: uninitialized (arbitrary values)
// This is safe because you get the whole array
// The SmallVec tracked only 2 elements, but array is size 4
// Access only the valid portion:
let len = 2;
for i in 0..len {
println!("Element {}: {}", i, array[i]);
}
// Or track the length before conversion:
let small: SmallVec4 = smallvec::smallvec![10, 20];
let len = small.len();
let array = small.into_inner().unwrap();
// Use only first `len` elements
}into_inner returns the full array including uninitialized elements beyond len().
Practical Use Cases
use smallvec::SmallVec;
fn use_cases() {
type SmallVec8 = SmallVec<[u8; 8]>;
// Use into_vec when:
// 1. You need a Vec regardless of size
fn process_vec(vec: Vec<u8>) {
// Function expects Vec
}
let small: SmallVec8 = smallvec::smallvec![1, 2, 3];
process_vec(small.into_vec());
// 2. You're going to grow beyond inline capacity anyway
let mut vec: Vec<u8> = small.into_vec();
vec.extend(0..100); // Will exceed inline capacity
// 3. You don't care about allocation optimization
let simple: SmallVec8 = smallvec::smallvec![1, 2];
let vec = simple.into_vec(); // Simple, guaranteed Vec
// Use into_inner when:
// 1. You want to avoid heap allocation for small cases
fn process_array(arr: [u8; 8]) {
// Process without allocation
}
let small: SmallVec8 = smallvec::smallvec![1, 2, 3];
if let Ok(array) = small.into_inner() {
process_array(array); // No heap allocation
}
// 2. You're working with fixed-size buffers
let buffer: SmallVec8 = smallvec::smallvec![b'H', b'i'];
match buffer.into_inner() {
Ok(array) => {
// Use array directly
println!("Inline buffer: {:?}", &array[..2]);
}
Err(vec) => {
// Handle spilled case
println!("Heap buffer: {:?}", vec);
}
}
}Choose based on whether you need Vec flexibility or want to preserve inline storage.
Converting Between Representations
use smallvec::SmallVec;
fn conversions() {
type SmallVec4 = SmallVec<[i32; 4]>;
// SmallVec → Vec (always works)
let small: SmallVec4 = smallvec::smallvec![1, 2, 3];
let vec: Vec<i32> = small.into_vec();
// Vec → SmallVec
let vec = vec![1, 2, 3];
let small: SmallVec4 = SmallVec::from_vec(vec);
// Array → SmallVec
let array = [1, 2, 3];
let small: SmallVec4 = SmallVec::from_buf(array);
// SmallVec → Array (into_inner, conditional)
let small: SmallVec4 = smallvec::smallvec![1, 2, 3];
let result: Result<[i32; 4], Vec<i32>> = small.into_inner();
// The full round-trip:
let original: SmallVec4 = smallvec::smallvec![1, 2, 3];
let vec = original.into_vec();
let back: SmallVec4 = SmallVec::from_vec(vec);
// But note: into_vec may have allocated, losing inline optimization
}into_vec loses the inline optimization; into_inner preserves it when possible.
Performance Implications
use smallvec::SmallVec;
fn performance() {
type SmallVec8 = SmallVec<[u64; 8]>;
// Benchmark: into_vec vs into_inner for inline data
// into_vec on inline data:
// 1. Allocates heap buffer
// 2. Copies elements from inline storage to heap
// 3. Returns Vec pointing to heap
let small: SmallVec8 = smallvec::smallvec![1, 2, 3, 4];
let vec = small.into_vec();
// Cost: 1 heap allocation + 4 element copies
// into_inner on inline data:
// 1. Returns Ok(array) directly
// 2. No allocation, no copy
let small: SmallVec8 = smallvec::smallvec![1, 2, 3, 4];
let Ok(array) = small.into_inner() else { panic!() };
// Cost: 0 allocations, 0 copies
// When already spilled:
// into_vec: unwrap heap Vec, minimal cost
// into_inner: Err(unwrap heap Vec), same cost
// For hot paths with small vectors, into_inner is significantly faster
}into_inner avoids allocation for inline data; into_vec may allocate unnecessarily.
Safe Handling of into_inner
use smallvec::SmallVec;
fn safe_handling() {
type SmallVec4 = SmallVec<[i32; 4]>;
// Pattern 1: Unwrap with meaningful error
let small: SmallVec4 = smallvec::smallvec![1, 2];
let array = small.into_inner().expect("expected inline storage");
// Pattern 2: Handle both cases
let small: SmallVec4 = smallvec::smallvec![1, 2, 3, 4, 5];
let vec = small.into_inner().unwrap_or_else(|vec| {
// Already spilled, just use the Vec
vec
});
// Pattern 3: Check before converting
let small: SmallVec4 = smallvec::smallvec![1, 2];
if small.len() <= 4 && !small.spilled() {
let array = small.into_inner().unwrap();
// Process inline array
} else {
let vec = small.into_vec();
// Process heap vec
}
// Pattern 4: Always convert to Vec if you don't care
let small: SmallVec4 = smallvec::smallvec![1, 2];
let vec = small.into_vec(); // Simpler but may allocate
}Handle into_inner's Result appropriately for your use case.
Checking if Spilled
use smallvec::SmallVec;
fn check_spilled() {
type SmallVec4 = SmallVec<[i32; 4]>;
let inline: SmallVec4 = smallvec::smallvec![1, 2, 3];
assert!(!inline.spilled());
let spilled: SmallVec4 = smallvec::smallvec![1, 2, 3, 4, 5];
assert!(spilled.spilled());
// Use spilled() to decide strategy:
let small: SmallVec4 = smallvec::smallvec![1, 2, 3];
if small.spilled() {
// Already on heap, into_vec is cheap
let vec = small.into_vec();
} else {
// Inline, consider into_inner for no allocation
let array = small.into_inner().unwrap();
}
// Or use len() to predict:
if small.len() <= 4 {
// Might be inline (check spilled() for certainty)
}
}spilled() returns true if the SmallVec has heap-allocated storage.
Real-World Example: Parser Buffers
use smallvec::SmallVec;
// Parser that typically works with small strings
fn parse_input(input: &str) -> SmallVec<[char; 32]> {
let mut chars: SmallVec<[char; 32]> = SmallVec::new();
for ch in input.chars() {
if ch.is_ascii() {
chars.push(ch);
}
}
chars
}
fn process_parsed() {
let result = parse_input("hello world");
// If we know most inputs fit inline:
match result.into_inner() {
Ok(array) => {
// No allocation occurred during parsing
// Array contains the result (with uninitialized tail)
let valid: &[char] = &array[..11]; // Known length
println!("Inline: {:?}", valid);
}
Err(vec) => {
// Large input, spilled to heap
println!("Heap: {:?}", vec);
}
}
// If we just need a Vec and don't care:
let result = parse_input("hello");
let vec: Vec<char> = result.into_vec();
// Always Vec, but may have allocated unnecessarily
}Use into_inner when the inline case is common and you want to optimize for it.
Synthesis
Quick reference:
| Method | Return Type | Inline Case | Spilled Case | Allocation |
|---|---|---|---|---|
into_vec() |
Vec |
Allocates heap, copies | Returns existing heap | Always (if inline) |
into_inner() |
Result<[N], Vec> |
Ok(array) |
Err(vec) |
Never (if inline) |
Decision guide:
use smallvec::SmallVec;
fn decision_guide() {
type SmallVec4 = SmallVec<[i32; 4]>;
let small: SmallVec4 = smallvec::smallvec![1, 2, 3];
// Use into_vec when:
// - You need Vec regardless of size
// - Simplicity matters more than performance
// - You'll grow the collection beyond inline capacity
let vec = small.into_vec();
// Use into_inner when:
// - Inline case is common and worth optimizing
// - You want to avoid allocation in hot paths
// - You can handle Result or know it fits inline
match small.into_inner() {
Ok(array) => { /* use inline array, no allocation */ }
Err(vec) => { /* handle heap case */ }
}
// Best practice for hot paths:
if !small.spilled() {
// Inline, use into_inner to avoid allocation
let array = small.into_inner().unwrap();
// Process array
} else {
// Already spilled, either method works
let vec = small.into_vec();
// Process vec
}
}Key insight: into_vec and into_inner serve different purposes in the SmallVec lifecycle. into_vec always produces a Vec, converting inline elements to heap storage if necessary—this is simpler but may introduce an allocation for data that was previously stack-allocated. into_inner tries to preserve the inline storage by returning Ok(array) when elements fit, avoiding allocation entirely; it returns Err(vec) when elements have spilled to heap, requiring handling of the error case. The key difference is that into_vec unconditionally heap-allocates inline data while into_inner preserves it when possible. Use into_vec for simplicity when you need a Vec regardless, or when the inline optimization doesn't matter. Use into_inner in performance-critical code where avoiding allocation matters, handling the Result appropriately. The uninitialized tail in into_inner's array contains arbitrary values beyond len()—only access elements that were actually pushed. Note that when a SmallVec has already spilled to the heap, both methods are essentially equivalent since the heap allocation already exists.
