If you've ever wondered how .NET achieves near-zero-allocation performance for string parsing or buffer operations, the answer usually involves Span\<T\> and Memory\<T\>. These types let you work with slices of memory without copying data, and they're worth adding to your toolkit.
The Problem With Slicing
Let's say you're parsing a comma-separated string. The traditional approach looks something like this:
string csv = "alice,bob,charlie";
string[] parts = csv.Split(',');
foreach (string part in parts)
{
Console.WriteLine(part);
}
This works fine, but Split allocates a new array and a new string for each element. If you're doing this inside a tight loop or processing large payloads, those allocations add up fast — and the GC has to clean them all up.
Introducing Span<T>
Span\<T\> is a ref struct that represents a contiguous region of arbitrary memory. It could be a slice of an array, a stack-allocated buffer, or unmanaged memory. The key property: it never owns the memory itself, so it never triggers allocations on its own.
Here's the same parsing example, allocation-free:
ReadOnlySpan<char> csv = "alice,bob,charlie";
int start = 0;
for (int i = 0; i <= csv.Length; i++)
{
if (i == csv.Length || csv[i] == ',')
{
ReadOnlySpan<char> token = csv.Slice(start, i - start);
Console.WriteLine(token.ToString());
start = i + 1;
}
}
No array, no string allocations for the tokens themselves (just the final ToString() calls). ReadOnlySpan\<char\> can wrap a string literal directly.
Slicing Arrays
Span\<T\> shines when you need to pass a portion of an array to a method without copying it:
int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8 };
// Process just the middle chunk
Span<int> middle = numbers.AsSpan(2, 4); // [3, 4, 5, 6]
foreach (int n in middle)
Console.Write($"{n} ");
// Output: 3 4 5 6
Changes to middle affect the original array — it's a view, not a copy. That's both a feature and something to be mindful of.
middle[0] = 99;
Console.WriteLine(numbers[2]); // 99
Stack Allocation With stackalloc
One of Span\<T\>'s killer features is working with stack-allocated buffers via stackalloc. This avoids heap allocation entirely:
Span<byte> buffer = stackalloc byte[256];
// Use buffer for temp work
buffer[0] = 0xFF;
buffer[1] = 0xFE;
// Pass to a method expecting Span<byte>
ProcessBuffer(buffer);
Stack allocation is fast and GC-free. Just keep the size reasonable — the stack is limited and blowing it out will crash your app.
Why Span<T> Is a ref struct
Span\<T\> is a ref struct, which means it has some restrictions you'll bump into:
- It can't be stored in a field on a regular class or struct
- It can't be used as a generic type argument
- It can't be boxed or used across
awaitboundaries
These constraints exist because Span\<T\> might point to stack memory, and the GC doesn't track stack memory. If you need to store a reference to memory across async calls or in a class field, that's where Memory\<T\> comes in.
Introducing Memory<T>
Memory\<T\> is the async-friendly counterpart to Span\<T\>. It's a regular struct (not a ref struct), so it can be stored, passed around, and used across await points. The trade-off: it can only point to heap memory, not stack memory.
public async Task ProcessChunkAsync(Memory<byte> buffer)
{
// This is fine — Memory<T> crosses await boundaries
await Task.Delay(10);
Span<byte> span = buffer.Span;
for (int i = 0; i < span.Length; i++)
span[i] ^= 0xFF; // Flip all bits
}
You get the Span\<T\> view via the .Span property whenever you need to do the actual work.
Converting Between Them
You'll often start with one and need the other. Here's how they relate:
byte[] array = new byte[1024];
// Array → Span
Span<byte> span = array.AsSpan();
Span<byte> slice = array.AsSpan(100, 200);
// Array → Memory
Memory<byte> memory = array.AsMemory();
Memory<byte> memorySlice = array.AsMemory(100, 200);
// Memory → Span (only valid synchronously)
Span<byte> fromMemory = memory.Span;
The conversion from Memory\<T\> to Span\<T\> is cheap — just a property access.
A Real-World Example: Parsing Numbers
Here's a practical example: parsing a list of integers from a string without allocating intermediate substrings:
public static IEnumerable<int> ParseInts(ReadOnlySpan<char> input)
{
int start = 0;
for (int i = 0; i <= input.Length; i++)
{
if (i == input.Length || input[i] == ',')
{
ReadOnlySpan<char> token = input.Slice(start, i - start).Trim();
if (int.TryParse(token, out int value))
yield return value;
start = i + 1;
}
}
}
// Usage
foreach (int n in ParseInts("10, 20, 30, 40"))
Console.WriteLine(n);
No intermediate strings. No array from Split. The only allocations are the yielded integers, which are value types on the stack anyway.
When to Reach for These Types
You don't need Span\<T\> everywhere. Reach for it when:
- You're in a hot path where allocations matter (high-throughput parsers, serialisation, network buffers)
- You're working with large arrays and want to pass slices without copying
- You're using
stackallocfor small temp buffers
For everyday business logic — building a CRUD API, processing a handful of form fields, generating a report — the regular string and array APIs are just fine.
The Broader Picture
Span\<T\> and Memory\<T\> are part of a broader set of low-allocation APIs in .NET. You'll see them throughout the framework:
System.IO.PipelinesusesReadOnlySequence\<byte\>andMemory\<byte\>for async I/OSystem.Text.JsonusesSpan\<byte\>internally for fast UTF-8 parsingStreamReaderhas overloads that acceptMemory\<char\>instead of allocating stringsMemoryMarshallets you reinterpret memory as different types
Once you understand these types, a lot of the performance-oriented .NET APIs start making more sense.
Summary
Span\<T\> gives you a zero-allocation view into memory — arrays, stack buffers, or unmanaged memory — with full slice and index support. Memory\<T\> gives you the same thing but usable across await boundaries and in class fields. Together they're the foundation of high-performance C# code without unsafe pointers or manual memory management.
Start with Span\<T\> when you want to avoid copying slices of arrays. Graduate to Memory\<T\> when you need to store or pass those views asynchronously. And use stackalloc when you need a small scratch buffer and want to keep the GC out of it entirely.
