InlineArray and Stack-Allocated Collections: Eliminating Heap Overhead in Swift
You have a tight loop that processes millions of small, fixed-size vectors — say, RGB color triples or 3D vertex
coordinates — and Instruments shows that heap allocations and retain/release traffic are your top bottleneck. The data
never changes size. You know exactly how many elements you need. Yet every Array you create still pays the cost of a
heap buffer, a reference count, and CoW bookkeeping.
Swift 6.2 introduces InlineArray<Count, Element>, a fixed-size collection that lives entirely on the stack with zero
heap allocation. In this post, you will learn how InlineArray works under the hood, how to use it with Span for safe
zero-copy access, and when it delivers measurable gains over Array. We will not cover Span and MutableSpan in
depth — those have their own dedicated post.
Contents
- The Problem
- Introducing InlineArray
- Working with InlineArray
- InlineArray and Span: Zero-Copy Access
- Non-Copyable Element Support
- Performance Considerations
- When to Use (and When Not To)
- Summary
The Problem
Consider a render pipeline that processes color data for every frame in a Pixar-style animation. Each pixel is an RGB triple — always exactly three components, never more, never less.
struct PixelColor {
let components: [Float] // Always exactly 3 elements (R, G, B)
}
func processFrame(pixels: [PixelColor]) -> [PixelColor] {
pixels.map { pixel in
// Apply brightness adjustment
let adjusted = pixel.components.map { $0 * 1.2 }
return PixelColor(components: adjusted)
}
}
This code is correct, but it hides serious performance problems. Every PixelColor instance allocates a separate heap
buffer for its [Float] array. That means for a single 1920x1080 frame, you are paying for over two million heap
allocations, each with reference counting overhead and CoW bookkeeping. When you map over the components, you allocate
yet another heap buffer for the result.
The fundamental mismatch is that Array is designed for dynamically sized collections. It manages a growable heap
buffer, tracks a capacity separate from its count, and maintains a reference count so it can share storage through
copy-on-write. None of that machinery is needed when you know at compile time that you will always have exactly three
elements.
Before Swift 6.2, your options were limited: use a tuple (Float, Float, Float) (no Collection conformance, awkward
element access) or drop into UnsafeBufferPointer (unsafe, manual lifetime management). Neither is satisfying.
Introducing InlineArray
InlineArray is a fixed-size, inline-stored collection
introduced in SE-0453 and shipping in
Swift 6.2. Its two generic parameters are the count (an integer generic parameter) and the element type:
struct InlineArray<let Count: Int, Element>: ~Copyable, ~Escapable
The key insight is that Count is a value-level generic parameter — a compile-time constant integer, not a type. The
compiler knows the exact size of the collection at compile time and embeds the storage directly into the struct’s memory
layout. No heap allocation. No reference count. No CoW indirection.
Here is the pixel example rewritten with InlineArray:
struct PixelColor {
var components: InlineArray<3, Float>
}
let scarletRed = PixelColor(
components: InlineArray<3, Float>(repeating: 0.0)
)
The PixelColor struct is now a flat value type. Its memory layout is exactly 12 bytes (3 x 4 bytes for Float) with
no pointers, no heap buffer, and no metadata. When you copy a PixelColor, the compiler emits a plain memcpy — no
retain/release traffic.
Note:
InlineArrayrequires Swift 6.2 or later. If your project targets earlier toolchains, this API is unavailable.
Working with InlineArray
Initialization
InlineArray provides several ways to create instances. The simplest is repeating:, which fills every element with
the same value:
// All zeros -- useful for buffers you will fill later
var colorBuffer = InlineArray<4, Float>(repeating: 0.0)
// From a closure that receives the index
var rampValues = InlineArray<8, Int> { index in
index * 10
}
// rampValues contains: 0, 10, 20, 30, 40, 50, 60, 70
You can also initialize an InlineArray from a collection literal when the count matches:
let buzzLightyearRGB: InlineArray<3, UInt8> = [0, 128, 0]
let woodyBrownRGB: InlineArray<3, UInt8> = [139, 90, 43]
Warning: The literal must have exactly
Countelements. Providing fewer or more elements is a compile-time error — the compiler enforces the size invariant statically.
Element Access
InlineArray provides subscript access, for-in iteration, and many familiar collection-style APIs. It does not
formally conform to Sequence or Collection — a deliberate design choice to avoid implicit copies of the inline
storage that generic collection algorithms could trigger:
var pixarPalette = InlineArray<5, String>(repeating: "")
pixarPalette[0] = "Woody"
pixarPalette[1] = "Buzz"
pixarPalette[2] = "Jessie"
pixarPalette[3] = "Rex"
pixarPalette[4] = "Hamm"
// Subscript access
print(pixarPalette[0]) // "Woody"
// Iterate with for-in
for character in pixarPalette {
print(character)
}
// Access count and indices
print(pixarPalette.count) // 5
print(pixarPalette.isEmpty) // false
InlineArray provides count, isEmpty, indices, subscript access, and for-in iteration. For higher-order
operations like sorted(), filter(), or reduce(), pass the InlineArray to an Array initializer first. This
deliberate omission of Collection conformance prevents generic algorithms from implicitly copying the inline storage.
Value Semantics
InlineArray is a value type. Copies are independent:
var original = InlineArray<3, Int>(repeating: 42)
var copy = original
copy[0] = 99
print(original[0]) // 42 -- unaffected
print(copy[0]) // 99
42
99
Unlike Array, there is no copy-on-write optimization here — and none is needed. Because the storage is inline
(typically on the stack), copying is just a memcpy of Count * MemoryLayout<Element>.stride bytes. For small,
fixed-size collections this is cheaper than the retain/release pair that Array’s CoW requires.
InlineArray and Span: Zero-Copy Access
One of the most powerful patterns in Swift 6.2 is pairing InlineArray with
Span for zero-copy, bounds-checked access to contiguous
memory. Span is a non-escapable, non-copyable view into contiguous elements — think of it as a safe slice that
borrows memory without copying or retaining it.
func averageBrightness(
of pixels: borrowing InlineArray<3, Float>
) -> Float {
let span = pixels.span
var total: Float = 0.0
for i in 0..<span.count {
total += span[i]
}
return total / Float(span.count)
}
let wallERustColor = InlineArray<3, Float>(repeating: 0.0)
let brightness = averageBrightness(of: wallERustColor)
The span property provides a Span<Float> that borrows the InlineArray’s inline storage directly. No allocation, no
copy, no reference counting — just a pointer and a count, with lifetime safety enforced by the compiler through
~Escapable.
When you need to mutate elements in place, use MutableSpan via the mutableSpan property:
func applyGammaCorrection(
to pixels: inout InlineArray<3, Float>,
gamma: Float
) {
var mutableView = pixels.mutableSpan
for i in 0..<mutableView.count {
mutableView[i] = pow(mutableView[i], 1.0 / gamma)
}
}
var nemoOrangeColor: InlineArray<3, Float> = [1.0, 0.5, 0.0]
applyGammaCorrection(to: &nemoOrangeColor, gamma: 2.2)
Tip: For deeper coverage of
SpanandMutableSpan, including lifetime rules and bridging with C APIs, see Span and MutableSpan.
Non-Copyable Element Support
Unlike Array, InlineArray supports non-copyable (~Copyable) element types. This is significant for modeling
resources with unique ownership — file handles, GPU buffers, or any type where duplication would be a logic error.
struct GPUTexture: ~Copyable {
let textureID: UInt32
let label: String
init(id: UInt32, label: String) {
self.textureID = id
self.label = label
}
deinit {
print("Releasing GPU texture \(label) (ID: \(textureID))")
}
}
// Array<GPUTexture> would NOT compile --
// Array requires Copyable elements.
// InlineArray stores elements inline without copying.
var renderTargets = InlineArray<2, GPUTexture>(
repeating: GPUTexture(id: 0, label: "default")
)
// Simplified for clarity
This capability makes InlineArray the natural container for fixed-size collections of unique resources. Array cannot
hold ~Copyable types because its CoW storage model fundamentally requires the ability to copy elements.
Apple Docs:
InlineArray— Swift Standard Library
Performance Considerations
The performance advantage of InlineArray over Array comes from three sources: elimination of heap allocation,
elimination of reference counting, and improved data locality.
Heap Allocation Elimination
Every Array instance allocates a backing buffer on the heap. For a three-element [Float], that means a malloc
call, a heap metadata header, and a reference count field — roughly 64 bytes of overhead for 12 bytes of payload.
InlineArray<3, Float> uses exactly 12 bytes, stored inline in the containing value.
In a tight loop creating millions of small arrays, the allocation overhead dominates. Consider a simplified frame buffer:
// Array approach -- 2 million heap allocations per frame
let pixelCount = 1920 * 1080
var heapPixels = [InlineArray<3, Float>]()
heapPixels.reserveCapacity(pixelCount)
for i in 0..<pixelCount {
// Each InlineArray is a flat value -- no inner heap alloc
let brightness = Float(i) / Float(pixelCount)
heapPixels.append(
InlineArray<3, Float>(repeating: brightness)
)
}
With InlineArray as the element type, the outer Array makes a single heap allocation for its buffer, and each
element is a flat 12-byte value stored contiguously. Compare this with Array<Array<Float>>, which would require one
outer allocation plus 2,073,600 inner allocations.
Reference Counting Elimination
Array is a reference-counted type. Every time you pass an Array to a function, store it in a property, or capture it
in a closure, the runtime increments and decrements a reference count. These atomic operations are not free — they
involve memory barriers that inhibit CPU instruction reordering.
InlineArray is a pure value type. Passing it around involves copying bytes, not managing reference counts. On Apple
Silicon, a memcpy of 12-48 bytes is substantially cheaper than an atomic retain/release pair.
Data Locality
When you have an Array of Array<Float>, each inner array is a pointer to a separate heap allocation. Iterating over
the data involves pointer chasing — jumping to random memory locations, defeating the CPU’s prefetcher and causing
cache misses.
An Array of InlineArray<3, Float> stores all data contiguously. The CPU prefetcher can predict access patterns, and
cache lines are used efficiently. For data-intensive workloads like image processing, physics simulations, or animation
curves, this locality improvement alone can yield 2-5x throughput gains.
Tip: Use Instruments’ Allocations and Time Profiler instruments to measure the impact. Look at “All Heap Allocations” count and “Transient” vs. “Persistent” allocations to verify that
InlineArrayeliminates the inner allocations you expect.
When to Use (and When Not To)
InlineArray is not a drop-in replacement for Array. It excels in a specific niche: small, fixed-size collections in
performance-sensitive code paths.
| Scenario | Recommendation |
|---|---|
| Fixed-size math vectors (2D, 3D, 4D) | Use InlineArray. Known size, value semantics, no heap overhead. |
| Small lookup tables (< 64 elements) | Use InlineArray if the size is known at compile time. |
| Non-copyable element storage | Use InlineArray. The only stdlib container supporting ~Copyable. |
| Dynamically sized collections | Use Array. InlineArray count is fixed — no append or remove. |
| Large fixed-size buffers (> 256 elements) | Prefer Array or UnsafeBufferPointer. Stack cost is high. |
| Many function boundaries | Prefer Array with CoW or pass with borrowing/inout. |
| Expensive-to-copy elements | Use borrowing parameter convention or make the element ~Copyable. |
Size Guidelines
The stack is not infinite. On Apple platforms, the default main thread stack size is 1 MB (8 MB on macOS), and secondary
threads default to 512 KB. An InlineArray<1000, SomeStruct> where SomeStruct is 256 bytes would consume 250 KB of
stack per instance — a significant chunk.
As a rule of thumb, keep InlineArray total size under a few hundred bytes. If you need larger fixed-size buffers,
consider wrapping the InlineArray in a class or storing it on the heap explicitly.
Warning: Stack overflow from oversized
InlineArrayinstances will crash your app without a helpful error message. Profile with the Address Sanitizer enabled during development to catch these issues early.
Summary
InlineArray<Count, Element>is a fixed-size, stack-allocated collection that eliminates heap allocation, reference counting, and CoW overhead for small, compile-time-known collection sizes.- Pair
InlineArraywithSpanandMutableSpanfor zero-copy, bounds-checked access to its inline storage. - Unlike
Array,InlineArraysupports~Copyableelement types, making it the right container for unique resources. - Performance gains come from three sources: no heap allocation, no atomic retain/release, and contiguous memory layout that improves cache utilization.
- Keep total
InlineArraysize small (under a few hundred bytes) to avoid stack pressure. For large fixed-size buffers,ArrayorUnsafeBufferPointerremain better choices.
For safe, zero-copy access patterns over contiguous memory — including InlineArray storage — see
Span and MutableSpan. To understand the ownership model that enables non-copyable
element support, explore Noncopyable Types.