Skip to content

InlineArray in Swift 6.2: Fixed-Size Arrays Done Right

Published: 5 min read
Edit on GitHub

Swift 6.2 ships with InlineArray, a fixed-size array type that’s been years in the making. If you’ve ever wished for C-style T[N] arrays but with Swift’s safety guarantees, this is it.

SE-0453 is one of the most important additions to the standard library for performance-critical code.

Why We Need InlineArray

Swift’s Array is excellent for general use, but it comes with hidden costs:

For most apps, this is fine. But in performance-critical code - game engines, audio processing, embedded systems, tight loops - these costs add up.

func processPixel() {
    // This allocates on the heap, even for 4 integers
    let rgba = [255, 128, 64, 255]
}

The workaround has been tuples:

func processPixel() {
    let rgba = (255, 128, 64, 255)
    
    // But you can't iterate or index dynamically
    for i in 0..<4 {
        // error: cannot subscript tuple
        print(rgba[i])
    }
}

Tuples don’t support subscripting or iteration. You’re stuck with .0, .1, .2, .3.

InlineArray solves both problems - inline storage with full indexing and iteration:

func processPixel() {
    let rgba: InlineArray<4, UInt8> = [255, 128, 64, 255]
    
    for i in rgba.indices {
        print(rgba[i])  // Works!
    }
}

No heap allocation. No reference counting. The storage is laid out inline - on the stack for local variables, or inline within a struct/class for properties.

The Generic Signature

The type signature is InlineArray<Count, Element> - count comes first. This reads naturally: “an InlineArray of 4 integers.”

Multi-dimensional arrays are intuitive too:

// A 3x4 matrix
let matrix: InlineArray<3, InlineArray<4, Float>> = [
    [1, 0, 0, 0],
    [0, 1, 0, 0],
    [0, 0, 1, 0]
]

// Access as matrix[row][col] - matches the declaration order
let value = matrix[2][3]

Memory Layout

InlineArray has predictable, zero-overhead memory layout:

MemoryLayout<InlineArray<4, UInt8>>.size      // 4
MemoryLayout<InlineArray<4, UInt8>>.stride    // 4
MemoryLayout<InlineArray<4, UInt8>>.alignment // 1

MemoryLayout<InlineArray<4, Int64>>.size      // 32
MemoryLayout<InlineArray<4, Int64>>.stride    // 32
MemoryLayout<InlineArray<4, Int64>>.alignment // 8

Size equals Element.stride * count. No hidden overhead.

Initialization

Three ways to create an InlineArray:

Literal syntax (compiler magic, not ExpressibleByArrayLiteral):

let numbers: InlineArray<3, Int> = [1, 2, 3]

// Type inference works too
let inferred: InlineArray = [1, 2, 3]  // InlineArray<3, Int>

Closure-based (great for computed values):

// Index-based initialization (note: no argument label)
let squares = InlineArray<5, Int> { i in i * i }
// [0, 1, 4, 9, 16]

// Chain from previous element
let values = InlineArray<10, Int>(first: 1) { prev in 
    prev * 2
}

Repeating value:

let zeros = InlineArray<100, Int>(repeating: 0)

Noncopyable Elements

InlineArray supports noncopyable types:

let atomics: InlineArray<4, Atomic<Int>> = [
    Atomic(0), 
    Atomic(1), 
    Atomic(2), 
    Atomic(3)
]

let mutexes = InlineArray<3, Mutex<Data>> { _ in 
    Mutex(Data()) 
}

This is huge for systems programming. You can have a fixed-size array of atomics, mutexes, or file handles without heap allocation.

Why No Sequence or Collection?

This is deliberate and important to understand. Unlike Array, InlineArray is eagerly copied - there’s no copy-on-write. The Language Steering Group specifically chose the name “Inline” to highlight this performance characteristic.

From the acceptance discussion:

Top of mind for the Language Steering Group was the behavior of this type regarding copies: namely, that this type is eagerly copied rather than being copy-on-write which carries substantial performance implications.

Conforming to Collection would enable implicit copies through slicing and generic algorithms, which defeats the purpose. Instead, use indices:

let data: InlineArray<1000, Float> = ...

for i in data.indices {
    process(data[i])
}

Primitives: Span Integration

A lesser-known feature: InlineArray exposes .span and .mutableSpan properties from SE-0447 - safe, bounds-checked access to the underlying memory without copying:

func processBuffer(_ span: Span<Float>) { ... }

var data: InlineArray<1024, Float> = ...
processBuffer(data.span)  // Zero-copy view into the array

This bridges InlineArray to APIs working with contiguous memory - crucial for C/C++ interop without dropping to UnsafeBufferPointer.

Must Be Fully Occupied

An InlineArray must always be fully initialized. You cannot have 2 elements in an InlineArray<3, Int> - all 3 slots must contain values.

C Interop - Not Yet

If you were hoping InlineArray would fix C array interop (importing char[16] as InlineArray<16, CChar> instead of a tuple), that’s been deferred pending further design work.

When Should You Use InlineArray?

InlineArray is a specialized tool, not a replacement for Array. For most application code, Array remains the right choice.

Use InlineArray when:

// Good: fixed-size math types
struct Vector3 {
    var values: InlineArray<3, Float>
}

// Good: embedded sensor buffer
struct SensorReadings {
    var samples: InlineArray<64, Int16>
}

// Good: noncopyable elements
let locks = InlineArray<4, Mutex<Data>> { _ in Mutex(Data()) }

Stick with Array when:

The overhead of Array is negligible for most apps. Reach for InlineArray when profiling shows allocation is a bottleneck, or when you’re in a domain (embedded, audio, games) where it matters by default.

The full proposal: SE-0453 InlineArray.