Inline Arrays: Fixed-Size Buffers Without Unsafe Code

C# has always had fixed-size buffers via the fixed keyword, but they required unsafe context and only worked with primitive types. C# 12 introduces inline arrays — a way to embed fixed-size arrays directly into structs using safe, managed code. They are the foundation for several runtime optimisations and are worth understanding even if you do not use them directly.

What They Are

An inline array is a struct decorated with [InlineArray(N)] containing a single field. The attribute tells the runtime to lay out N copies of that field contiguously in memory:

Example.cs
[System.Runtime.CompilerServices.InlineArray(8)]
public struct EightInts
{
    private int _element;
}

This creates a struct that contains exactly 8 integers, laid out sequentially in memory with no object header or array overhead. You access elements using the indexer:

Example.cs
var buffer = new EightInts();
buffer[0] = 42;
buffer[7] = 99;

// You can also get a span over the contents
Span<int> span = buffer;

Why They Exist

The primary motivation is performance. Regular arrays in .NET are heap-allocated objects with headers, bounds checking, and GC tracking. For small, fixed-size collections that live on the stack, this overhead is disproportionate.

Consider a method that needs a temporary buffer of 4 elements:

Example.cs
// Heap allocation, GC pressure
void ProcessWithArray()
{
    var buffer = new int[4];
    FillBuffer(buffer);
    ConsumeBuffer(buffer);
}

// Stack allocation, zero GC pressure
[InlineArray(4)]
struct FourInts { private int _e; }

void ProcessWithInlineArray()
{
    var buffer = new FourInts();
    Span<int> span = buffer;
    FillBuffer(span);
    ConsumeBuffer(span);
}

The inline array version keeps the data on the stack, produces no garbage, and avoids the array object header entirely.

How the Runtime Uses Them

You might not write [InlineArray] directly, but you benefit from it constantly. The runtime and BCL use inline arrays internally to optimise collection expressions and other features:

Example.cs
// When the compiler lowers this:
Span<int> values = [1, 2, 3];

// It may generate an inline array behind the scenes:
[InlineArray(3)]
struct __InlineArray3 { private int _e; }

var __buffer = new __InlineArray3();
__buffer[0] = 1;
__buffer[1] = 2;
__buffer[2] = 3;
Span<int> values = __buffer;

This is why small collection expressions targeting Span<T> can be stack-allocated — inline arrays make it possible.

Practical Uses

Small Ring Buffers

Example.cs
[InlineArray(16)]
public struct SampleBuffer
{
    private double _element;
}

public struct MovingAverage
{
    private SampleBuffer _samples;
    private int _index;
    private int _count;

    public void Add(double value)
    {
        _samples[_index % 16] = value;
        _index++;
        if (_count < 16) _count++;
    }

    public double Average()
    {
        ReadOnlySpan<double> span = _samples;
        double sum = 0;
        for (int i = 0; i < _count; i++)
            sum += span[i];
        return sum / _count;
    }
}

This entire structure lives on the stack (or inline in another struct) with no heap allocations.

Interop Buffers

Inline arrays are excellent for interop scenarios where you need fixed-size buffers matching C structures:

Example.cs
[InlineArray(256)]
public struct PathBuffer
{
    private char _element;
}

// Use it for P/Invoke calls expecting a fixed-size character buffer
public void GetTempPath(ref PathBuffer buffer)
{
    Span<char> span = buffer;
    // Fill span from native call...
}

Rules and Constraints

There are several constraints to be aware of:

Bounds checking still applies — accessing buffer[8] on an 8-element inline array throws an IndexOutOfRangeException, just like a regular array.

Should You Use Them Directly?

For most application developers, inline arrays are an implementation detail that powers other features like collection expressions and stackalloc optimisations. You benefit from them without writing [InlineArray] yourself.

Use them directly when you are writing performance-critical code with small, fixed-size buffers — particularly in game engines, parsers, protocol handlers, or numerical computing where avoiding GC pressure is essential. For everything else, let the compiler use them on your behalf.