Span and Memory — Zero-Allocation Slicing

Span<T> is a stack-only type that represents a contiguous region of memory — a slice of an array, a stack-allocated buffer, or native memory — without copying. It is the fundamental primitive for high-performance, zero-allocation data processing in .NET. Where previously you might call string.Substring() to extract a portion of text (allocating a new string), ReadOnlySpan<char> lets you represent and work with that slice without any allocation. ASP.NET Core’s HTTP parser, JSON parser, and routing engine are built on spans, which is a significant reason for their throughput.

Span Fundamentals

// ── Creating spans from arrays ─────────────────────────────────────────────
int[] array = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

Span<int>         full  = array;             // implicit conversion
Span<int>         slice = array.AsSpan(2, 5);  // elements [2..7) = {3,4,5,6,7}
ReadOnlySpan<int> ro    = array.AsSpan(0, 3);  // read-only view: {1,2,3}

// Modify through span — modifies the original array
slice[0] = 99;
Console.WriteLine(array[2]);   // 99 — span and array share the same memory

// ── String spans — zero-allocation slicing ────────────────────────────────
string csv = "alice@example.com,bob@test.org,carol@web.net";

ReadOnlySpan<char> span = csv.AsSpan();
int comma = span.IndexOf(',');

ReadOnlySpan<char> first  = span[..comma];          // "alice@example.com" — no allocation!
ReadOnlySpan<char> rest   = span[(comma + 1)..];    // "bob@test.org,carol@web.net"

// Compare without allocating
bool isAlice = first.Equals("alice@example.com", StringComparison.Ordinal);

// Convert to string only when you actually need a string (at boundary)
string firstStr = first.ToString();   // one allocation at the boundary
Note: Span<T> is a ref struct — it can only exist on the stack. This means you cannot store a Span<T> in a class field, in a generic type parameter, in an array, or use it across an await boundary. The stack-only constraint is enforced by the compiler. When you need a slice that survives beyond the current method (e.g., stored in a field or passed across async boundaries), use Memory<T> instead — it is a heap-compatible equivalent that wraps the same underlying data.
Tip: The high-performance CSV or log parsing pattern: receive a large string or byte array, convert to a span once, then slice and process the span iteratively without any allocations. Only convert span slices back to strings when you genuinely need a string (e.g., as a dictionary key or to return to a caller). This pattern can reduce allocations by 90%+ in parsing-heavy code paths. ASP.NET Core’s HTTP/2 header parsing uses exactly this approach.
Warning: You cannot use Span<T> in async methods — the compiler will reject it. The restriction is: if a method uses await, it becomes a state machine stored on the heap, and Span<T> cannot be stored on the heap. The solution is to separate the span-based synchronous work into a non-async method, then call that method from the async method. The async method receives the result (a value type or a string), while the sync method does all the span manipulation.

Memory<T> — Heap-Compatible Slice

// Memory<T> — can be stored in fields, arrays, passed across async
public class BufferedReader
{
    private readonly Memory<byte> _buffer;   // stored as a field — impossible with Span

    public BufferedReader(byte[] data)
        => _buffer = data.AsMemory();   // Memory wraps the same array, no copy

    public async Task ProcessAsync(CancellationToken ct)
    {
        // Get a span from Memory for the actual work (inside sync block)
        ReadOnlySpan<byte> span = _buffer.Span;   // ← Span only here, not stored

        // Can await inside an async method because we're using Memory, not Span
        await SaveResultAsync(ParseHeader(span), ct);
    }

    private string ParseHeader(ReadOnlySpan<byte> span)
    {
        // Span-based parsing — synchronous, no await, no heap allocation
        return System.Text.Encoding.UTF8.GetString(span[..16]);
    }
}

// ── Memory.Slice ───────────────────────────────────────────────────────────
byte[] data = new byte[1024];
Memory<byte> mem   = data.AsMemory();
Memory<byte> first = mem.Slice(0, 512);    // first 512 bytes
Memory<byte> last  = mem[512..];           // last 512 bytes

// Convert to Span when doing the actual processing
ProcessData(first.Span);   // Span used only in this synchronous call

Parsing Without Allocation

// Parse CSV line into fields without allocating strings for each field
public static List<string> ParseCsvLine(string line)
{
    var fields  = new List<string>();
    var span    = line.AsSpan();

    while (!span.IsEmpty)
    {
        int comma = span.IndexOf(',');
        if (comma == -1)
        {
            fields.Add(span.Trim().ToString());   // last field
            break;
        }
        fields.Add(span[..comma].Trim().ToString());   // allocate only at boundary
        span = span[(comma + 1)..];                    // advance without allocation
    }
    return fields;
}

Common Mistakes

Mistake 1 — Storing Span in a class field (compile error)

❌ Wrong — compile error: cannot store ref struct in a heap type:

public class Parser { private Span<byte> _buffer; }  // compile error!

✅ Correct — store Memory<byte> in the field; get Span<byte> when needed:

public class Parser { private Memory<byte> _buffer; }

Mistake 2 — Using Span across an await boundary (compile error)

❌ Wrong — compile error inside an async method:

async Task ProcessAsync()
{
    Span<byte> span = buffer.AsSpan();
    await SomeWorkAsync();   // compile error — Span cannot survive across await
}

✅ Correct — extract span work into a synchronous helper method.

🧠 Test Yourself

Why does span[..comma] not allocate memory while str.Substring(0, comma) does?