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.