Swift InlineArray and Span: A Complete Guide to High-Performance, Memory-Safe Collections

Swift 6.2 introduced InlineArray for stack-allocated, fixed-size arrays and Span for safe, zero-overhead memory views. Learn the syntax, memory layout, practical patterns, performance benchmarks, and best practices — including Swift 6.3 Embedded Swift support.

Swift 6.2 introduced two types that fundamentally change how you work with memory in performance-critical code: InlineArray and Span. Together, they let you ditch heap allocations for fixed-size data and safely access contiguous memory — no unsafe pointers required. And with Swift 6.3 (released March 2026) extending their support to Embedded Swift and improving LLDB debugging, these types are genuinely production-ready across every Swift platform.

This guide walks you through everything you need to build with InlineArray and Span — basic syntax, real-world patterns, performance characteristics, and the pitfalls that'll trip you up if you're not careful.

What Problem Do InlineArray and Span Solve?

Swift's standard Array is great for everyday work. It grows dynamically, supports copy-on-write, and conforms to Sequence and Collection. But all that flexibility comes at a cost:

  • Heap allocation — every Array stores its elements in a heap-allocated buffer, even if you only need four items.
  • Reference counting — the buffer is reference-counted, adding overhead on every copy and scope exit.
  • Copy-on-write checks — before every mutation, Swift has to check whether the buffer is uniquely referenced.

For most app code, these costs are negligible. You honestly won't notice them.

But in tight loops, real-time audio processing, graphics pipelines, and embedded systems? They add up fast. Before Swift 6.2, the only escape hatch was UnsafeBufferPointer — fast but dangerous. InlineArray and Span close this gap by offering zero-overhead alternatives that stay memory-safe.

InlineArray: Fixed-Size, Stack-Allocated Arrays

Declaration and Initialization

InlineArray is a generic struct whose size is baked into its type, powered by Swift's value generics feature (SE-0452):

// Full generic syntax
let rgb: InlineArray<3, UInt8> = [255, 128, 0]

// Shorthand "of" syntax (preferred)
let rgba: [4 of UInt8] = [255, 128, 0, 255]

// Type inference — Swift deduces both count and element type
let names: InlineArray = ["Alice", "Bob", "Charlie"]

You can also create an array filled with a repeating value:

// All zeros
let buffer = InlineArray<64, UInt8>(repeating: 0)

// A 4x4 identity-style grid seed
let grid: [16 of Float] = .init(repeating: 0.0)

Reading and Writing Elements

Subscript access works exactly like Array, with bounds checking on by default:

var pixel: [4 of UInt8] = [255, 128, 0, 255]

// Read
let red = pixel[0]   // 255
let alpha = pixel[3]  // 255

// Write
pixel[1] = 200

// Bounds-checked — this traps at runtime:
// let crash = pixel[4]  // Fatal error: Index out of range

For those performance-critical inner loops where you've already validated your indices, there's an unchecked subscript that skips bounds checking:

// Only use when you can guarantee index validity
let value = pixel[unchecked: 2]  // No bounds check

Use this sparingly. The performance gain is real but the safety tradeoff is significant — a bad index here is undefined behavior, not a clean crash.

Iterating Over Elements

InlineArray intentionally does not conform to Sequence or Collection. This might seem surprising at first, but it's a deliberate design choice — keeping the type minimal avoids protocol witness table overhead. To iterate, use the indices property:

let colors: [4 of String] = ["red", "green", "blue", "alpha"]

for i in colors.indices {
    print(colors[i])
}

If you need Collection methods like map or filter, convert to a Span first (we'll get to that shortly) or iterate with indices and accumulate results manually.

Memory Layout

Here's where things get interesting. The key advantage of InlineArray is where its data lives:

  • Local variable → stored on the stack
  • Stored property of a struct → inline within the struct
  • Stored property of a class → inline within the class's heap allocation (no separate buffer)
struct Particle {
    var position: [3 of Float]  // 12 bytes, inline in the struct
    var velocity: [3 of Float]  // 12 bytes, inline in the struct
    var color: [4 of UInt8]     // 4 bytes, inline in the struct
}

// Total: 28 bytes, no heap allocations, no reference counting
// Compare with Array-based version: 3 separate heap buffers + RC overhead

Think about that for a second — a million Particle instances with regular Array properties would mean three million separate heap allocations. With InlineArray, it's all packed tightly into the struct. This layout is ideal for data flowing through SIMD operations, GPU upload buffers, or tightly-packed network packets.

Span: Safe, Non-Owning Memory Views

What Is a Span?

Span provides a safe, bounds-checked view over contiguous memory. Think of it as a read-only window into an existing buffer — whether that buffer is an Array, InlineArray, ContiguousArray, or even memory coming from a C API.

Unlike UnsafeBufferPointer, Span enforces memory safety at compile time with zero runtime overhead:

let scores: [4 of Int] = [95, 87, 92, 78]
let span: Span<Int> = scores.span

print(span[0])     // 95
print(span.count)  // 4

Lifetime Safety Rules

Span is a non-escapable type — the compiler straight-up prevents it from outliving the memory it borrows. This eliminates use-after-free bugs at compile time, which is (honestly) one of my favorite things about the design:

// ✅ This is fine — span is used within the lifetime of data
func process() {
    let data: [8 of UInt8] = [1, 2, 3, 4, 5, 6, 7, 8]
    let span = data.span
    computeChecksum(span)
}

// ❌ Compiler error — cannot return a Span that borrows a local
func broken() -> Span<UInt8> {
    let data: [8 of UInt8] = [1, 2, 3, 4, 5, 6, 7, 8]
    return data.span  // Error: span cannot escape
}

// ❌ Compiler error — cannot capture in a closure
func alsoBroken() {
    let data: [4 of Int] = [1, 2, 3, 4]
    let span = data.span
    DispatchQueue.global().async {
        print(span[0])  // Error: cannot capture non-escapable type
    }
}

These constraints are checked entirely at compile time. No runtime wrapper, no reference counting, no performance penalty. Just safety you don't have to think about.

Writing Generic Functions with Span

The real power of Span shows up when you write functions that operate on any contiguous storage without caring about the underlying container type:

func sum(_ values: Span<Int>) -> Int {
    var total = 0
    for i in values.indices {
        total += values[i]
    }
    return total
}

// Works with Array
let arrayData = [10, 20, 30]
sum(arrayData.span)  // 60

// Works with InlineArray
let inlineData: [3 of Int] = [10, 20, 30]
sum(inlineData.span)  // 60

// Works with ContiguousArray
let contiguousData: ContiguousArray = [10, 20, 30]
sum(contiguousData.span)  // 60

This pattern replaces the need for generic <C: Collection> constraints when you only need contiguous element access. And it avoids the protocol dispatch overhead that comes with those generic constraints — a nice bonus for hot paths.

InlineArray + Span in Practice

Example: RGBA Pixel Processing

Image processing is honestly a perfect fit for these types. Each pixel is a fixed-size structure, and operations run millions of times per frame:

struct Pixel {
    var channels: [4 of UInt8]  // R, G, B, A — 4 bytes, stack-allocated
    
    var red: UInt8 { channels[0] }
    var green: UInt8 { channels[1] }
    var blue: UInt8 { channels[2] }
    var alpha: UInt8 { channels[3] }
    
    func brighten(by factor: Float) -> Pixel {
        var result = self
        for i in 0..<3 {  // Skip alpha
            let brightened = Float(channels[i]) * factor
            result.channels[i] = UInt8(min(brightened, 255))
        }
        return result
    }
}

func applyBrightness(to pixels: inout [Pixel], factor: Float) {
    for i in pixels.indices {
        pixels[i] = pixels[i].brighten(by: factor)
    }
}

No heap allocations per pixel. No reference counting. Just raw, predictable performance.

Example: Fixed-Size Ring Buffer

Ring buffers for sensor data, audio samples, or network packets benefit enormously from zero-allocation storage:

struct RingBuffer {
    private var storage: [256 of Float]
    private var writeIndex: Int = 0
    private var count: Int = 0
    
    init() {
        storage = .init(repeating: 0.0)
    }
    
    mutating func push(_ value: Float) {
        storage[writeIndex] = value
        writeIndex = (writeIndex + 1) % 256
        count = min(count + 1, 256)
    }
    
    func latest(_ n: Int) -> [Float] {
        precondition(n <= count)
        var result: [Float] = []
        result.reserveCapacity(n)
        for i in 0..<n {
            let index = (writeIndex - n + i + 256) % 256
            result.append(storage[index])
        }
        return result
    }
}

The entire 256-element buffer lives inside the struct. If RingBuffer is a local variable, that's 1 KB on the stack — well within safe limits and completely allocation-free.

Example: Type-Safe Network Packet Header

When parsing binary protocols, InlineArray maps cleanly to fixed-size fields:

struct IPv4Header {
    var versionAndIHL: UInt8
    var dscp: UInt8
    var totalLength: UInt16
    var identification: UInt16
    var flagsAndOffset: UInt16
    var ttl: UInt8
    var protocolNumber: UInt8
    var checksum: UInt16
    var sourceIP: [4 of UInt8]       // 4 bytes, inline
    var destinationIP: [4 of UInt8]  // 4 bytes, inline
}

func parseHeader(from bytes: Span<UInt8>) -> IPv4Header? {
    guard bytes.count >= 20 else { return nil }
    // Parse fields from the span safely...
    // Span guarantees we won't read out of bounds
    return nil // Simplified for illustration
}

The combination of InlineArray for the struct layout and Span for safe parsing is really elegant here. You get the exact memory layout you need without any unsafe code.

Performance: Array vs InlineArray vs Span

So, how much faster are we actually talking? Benchmark results consistently show InlineArray delivering 20–30% faster access in tight loops compared to Array. The improvement comes from eliminating three sources of overhead:

Overhead SourceArrayInlineArraySpan
Heap allocationYesNoNo (borrows)
Reference countingYesNoNo
Copy-on-write checkEvery mutationNeverN/A (read view)
Bounds checkingYesYes (opt-out available)Yes

The performance gap is most noticeable when:

  • Element count is small (under ~64 elements) — heap allocation overhead dominates at this scale
  • Elements are value types — no additional reference counting per element
  • Access is in a hot loop — the compiler can keep InlineArray data in registers or L1 cache
  • Many instances exist — a million Particle structs with inline vectors avoid a million heap buffers

For large collections (thousands of elements), stick with Array. The stack has limited space, and oversized InlineArray instances will crash your program with a stack overflow.

When to Use Each Type

Use Array When

  • The number of elements isn't known at compile time
  • You need to append, remove, or resize
  • You need Sequence/Collection conformance
  • Elements are large or numerous (hundreds+)

Use InlineArray When

  • The size is known at compile time and small (typically under 64 elements)
  • You need to avoid heap allocations (real-time audio, frame-rate-sensitive rendering)
  • You're modeling fixed-size data — colors, coordinates, packet headers
  • You're working in Embedded Swift where heap allocation may not even be available

Use Span When

  • You're writing a function that reads contiguous data from any container
  • You want to replace UnsafeBufferPointer with something safe
  • You're interfacing with C/C++ code that uses std::span
  • You need zero-copy access to a buffer without taking ownership

InlineArray and Span in Swift 6.3 and Embedded Swift

Swift 6.3 (released March 24, 2026) brought some welcome improvements for both types, especially in Embedded Swift environments:

  • LLDB now natively supports InlineArray — you can inspect values directly in the debugger without fighting the expression evaluator (finally!)
  • Full String APIs are now available in Embedded Swift alongside InlineArray and Span
  • Floating-point printing works for Float and Double in Embedded Swift, so description and debugDescription work on InlineArray<N, Float> values
  • The new EmbeddedRestrictions diagnostic group warns about language constructs unavailable in Embedded Swift, helping you catch issues early

If you're targeting microcontrollers or other resource-constrained platforms, InlineArray and Span are essentially the collection types you should reach for. They require no heap allocator and produce completely predictable memory layouts.

Common Pitfalls and Best Practices

1. Don't Use InlineArray for Large Buffers

Stack space is limited (typically 1–8 MB depending on the platform). An InlineArray<10000, SomeLargeStruct> can overflow the stack silently — and that's a crash with no helpful error message:

// ❌ Dangerous — may overflow the stack
let hugeBuffer: [10000 of SomeStruct] = .init(repeating: .init())

// ✅ Use Array for large, dynamically-sized data
let hugeBuffer = Array(repeating: SomeStruct(), count: 10000)

2. Use Span for Read-Only Access Patterns

Span is read-only by design. If you need mutable access, reach for MutableSpan, which enforces exclusivity at compile time:

func zero(out buffer: inout [8 of UInt8]) {
    var mutableSpan = buffer.mutableSpan
    for i in mutableSpan.indices {
        mutableSpan[i] = 0
    }
}

3. Prefer the Shorthand Syntax

The [N of Element] syntax reads more naturally and is the preferred style in the Swift community:

// ✅ Preferred
var matrix: [9 of Float] = .init(repeating: 0.0)

// Also valid but more verbose
var matrix: InlineArray<9, Float> = .init(repeating: 0.0)

4. Combine with @c for C Interop

In Swift 6.3, the new @c attribute pairs nicely with InlineArray for exposing Swift functions to C that accept or return fixed-size data:

@c(process_pixel)
public func processPixel(_ pixel: [4 of UInt8]) -> [4 of UInt8] {
    var result = pixel
    // Invert RGB, keep alpha
    for i in 0..<3 {
        result[i] = 255 - pixel[i]
    }
    return result
}

Frequently Asked Questions

When should I use InlineArray instead of a tuple?

Pretty much whenever you'd use a tuple of identical types. Unlike tuples, InlineArray supports subscript access, bounds checking, and the indices property. A tuple (Float, Float, Float) forces you to access elements as .0, .1, .2 — with InlineArray<3, Float> you can use [i] with a variable index, which is far more practical in loops.

Can I use InlineArray with SwiftUI?

Yes, but with a caveat. InlineArray doesn't conform to RandomAccessCollection, so you can't pass it directly to ForEach. Convert it to an Array first, or iterate using indices in your view body. For most SwiftUI code, Array is still the better choice since view building isn't a performance-critical hot path.

Is Span the same as C++'s std::span?

Same concept — a non-owning view over contiguous memory — but Swift's Span is meaningfully safer. It enforces lifetime dependencies at compile time through non-escapability, preventing dangling references entirely. Swift 6.2+ also supports direct interop: you can pass C++'s std::span into Swift as a Span automatically.

Does InlineArray work with Codable?

Not yet. InlineArray doesn't currently conform to Codable. If you need to serialize fixed-size data, you'll have to encode and decode through an intermediate Array, or implement Codable conformance manually on your containing type. It's a bit tedious but straightforward.

What happens if I make an InlineArray too large?

Since InlineArray is stored on the stack (when used as a local variable), an excessively large instance — say [100000 of Int] — will overflow the stack and crash your program. Keep inline arrays small (generally under a few hundred elements) and use Array for larger datasets. There's no compiler warning for this, so it's on you to be mindful of the size.

About the Author Editorial Team

Our team of expert writers and editors.