C# Span<T> and Memory<T> — zero-allocation slicing and high-performance buffers
Span<T> (C# 7.2+) is a stack-only, ref struct that points to a contiguous block of memory — an array, a string, a stack-allocated buffer, or native memory — without copying any data. You get a view into the original memory, not a new object.
Memory<T> is the heap-safe sibling: it can be stored in fields, captured in lambdas, and used across await boundaries where Span<T> cannot.
Use them whenever you want to eliminate intermediate array or string allocations in hot paths: parsers, serializers, protocol implementations, and any code that slices large buffers repeatedly.
Why Span<T> Exists
Without Span<T>, every slice creates a new allocation:
string line = "Alice,30,Engineer";
// Old approach — two new string allocations
string[] parts = line.Split(','); // allocates string[] + 3 strings
string name = parts[0]; // points to an already-allocated string
// Span approach — zero allocations
ReadOnlySpan<char> span = line.AsSpan();
ReadOnlySpan<char> nameSpan = span[..span.IndexOf(',')]; // no new string
Console.WriteLine(nameSpan.ToString()); // Alice (allocation only when you actually need a string)Creating a Span<T>
// From an array
int[] numbers = { 1, 2, 3, 4, 5 };
Span<int> all = numbers; // implicit conversion
Span<int> slice = numbers.AsSpan(1, 3); // { 2, 3, 4 } — no copy
// From a portion of an array using range syntax
Span<int> first3 = numbers.AsSpan()[..3]; // { 1, 2, 3 }
Span<int> last2 = numbers.AsSpan()[^2..]; // { 4, 5 }
// Modifying through a Span modifies the original array
slice[0] = 99;
Console.WriteLine(numbers[1]); // 99 ← original changedReadOnlySpan<T> for Strings
Strings are immutable, so use ReadOnlySpan<char> when slicing text.
string csv = "2026-05-02,USD,1234.56";
ReadOnlySpan<char> remaining = csv.AsSpan();
// Parse date segment
int comma1 = remaining.IndexOf(',');
ReadOnlySpan<char> datePart = remaining[..comma1];
remaining = remaining[(comma1 + 1)..];
// Parse currency segment
int comma2 = remaining.IndexOf(',');
ReadOnlySpan<char> currency = remaining[..comma2];
remaining = remaining[(comma2 + 1)..];
// Parse amount — directly parse numeric value, no string conversion needed
decimal amount = decimal.Parse(remaining); // .NET 6+ overload accepts ReadOnlySpan<char>
Console.WriteLine(datePart.ToString()); // 2026-05-02
Console.WriteLine(currency.ToString()); // USD
Console.WriteLine(amount); // 1234.56Stack Allocation with stackalloc
Span<T> is the only safe way to use stackalloc outside an unsafe context. Stack memory is extremely fast and produces zero GC pressure.
// Allocate 128 bytes on the stack — no heap allocation, no GC
Span<byte> buffer = stackalloc byte[128];
buffer.Fill(0); // zero-initialize
// Use it like a regular span
buffer[0] = 0x48; // 'H'
buffer[1] = 0x69; // 'i'
Console.WriteLine(buffer.Length); // 128
// Rule of thumb: keep stackalloc small (< 1 KB) to avoid stack overflow
// For larger or variable-size buffers, use ArrayPool<T> insteadArrayPool<T> — Reusable Heap Buffers
When you need a buffer larger than a few hundred bytes, rent it from ArrayPool<T> instead of allocating.
using System.Buffers;
byte[] rented = ArrayPool<byte>.Shared.Rent(4096); // reused from pool
try
{
Span<byte> buffer = rented.AsSpan(0, 4096);
// ... fill and use buffer ...
buffer.Fill(42);
Console.WriteLine(buffer[0]); // 42
}
finally
{
ArrayPool<byte>.Shared.Return(rented); // return to pool — must always return
}Memory<T> — the Async-Compatible Alternative
Span<T> is a ref struct, which means it cannot:
- Be a field in a class or regular struct
- Be captured in a lambda or local function
- Be used across an
awaitboundary
Use Memory<T> when you need any of those capabilities.
// Memory<T> can be stored in a class field, passed to async methods, etc.
public class DataProcessor
{
private readonly Memory<byte> _buffer;
public DataProcessor(byte[] data)
{
_buffer = data.AsMemory(); // wrap once, reuse freely
}
public async Task ProcessAsync()
{
// .Span property gives a Span<T> for synchronous work
Span<byte> sync = _buffer.Span;
sync[0] = 0xFF;
await Task.Delay(1); // can cross await — Span<T> could not
// Slice Memory<T> just like Span<T>
Memory<byte> firstHalf = _buffer[..(_buffer.Length / 2)];
Console.WriteLine(firstHalf.Length);
}
}Slicing Syntax Recap
Both Span<T> and Memory<T> support the same range/index operators introduced in C# 8.
int[] data = { 10, 20, 30, 40, 50 };
Span<int> s = data;
// Index from end
Console.WriteLine(s[^1]); // 50 — last element
// Range slice (returns a new Span pointing into the same memory)
Span<int> middle = s[1..4]; // { 20, 30, 40 }
Span<int> fromTwo = s[2..]; // { 30, 40, 50 }
Span<int> toThree = s[..3]; // { 10, 20, 30 }
// The Slice() method is equivalent and explicit
Span<int> explicit = s.Slice(1, 3); // start=1, length=3 → { 20, 30, 40 }Span<T> vs Memory<T> vs Array — Quick Comparison
T[] (array) | Span<T> | Memory<T> | |
|---|---|---|---|
| Allocation | Heap | None (view) | None (view) |
| GC pressure | Yes | No | No |
| Stackalloc compatible | No | Yes | No |
| Can be a class field | Yes | No | Yes |
Works across await | Yes | No | Yes |
| Works with strings | Via ToCharArray() | Via AsSpan() | Via AsMemory() |
| Performance | Good | Best | Very good |
| Available since | Always | C# 7.2 | C# 7.2 |
Span<T> Limitations
// ❌ Cannot be a field in a class
public class Bad
{
private Span<int> _span; // compile error: 'Span<int>' cannot be used as a field
}
// ❌ Cannot cross an await boundary
async Task BadAsync()
{
Span<int> span = stackalloc int[10];
await Task.Delay(1); // compile error: cannot use Span<T> in async method
}
// ✅ Use Memory<T> instead for async methods
async Task GoodAsync()
{
int[] buffer = new int[10];
Memory<int> mem = buffer;
await Task.Delay(1); // fine
}
// ✅ Or limit Span<T> to the synchronous portion
async Task AlsoGoodAsync()
{
int[] buffer = new int[10];
FillBuffer(buffer.AsSpan()); // synchronous helper uses Span<T>
await SaveAsync(buffer); // pass the array across await, not the span
}Real-World Example: Zero-Allocation CSV Row Parser
A practical parser that splits a CSV line into fields without allocating a string[] or any intermediate strings.
using System;
using System.Collections.Generic;
/// <summary>
/// Splits a CSV line into ReadOnlyMemory<char> segments — zero string allocations.
/// </summary>
static List<ReadOnlyMemory<char>> ParseCsvRow(string line, char delimiter = ',')
{
var fields = new List<ReadOnlyMemory<char>>();
ReadOnlyMemory<char> remaining = line.AsMemory();
while (remaining.Length > 0)
{
int idx = remaining.Span.IndexOf(delimiter);
if (idx < 0)
{
fields.Add(remaining);
break;
}
fields.Add(remaining[..idx]);
remaining = remaining[(idx + 1)..];
}
return fields;
}
// Usage
var fields = ParseCsvRow("Alice,30,Engineer,London");
foreach (var field in fields)
Console.WriteLine(field); // Prints each field without creating new strings
// Alice
// 30
// Engineer
// London
// Only allocate a string when actually needed downstream
string name = fields[0].ToString(); // one allocation, on demandWhen to Use Each
Span<T>— synchronous hot paths, stack-based scratch buffers, slicing arrays/strings inside a single method call.Memory<T>— async I/O pipelines, data stored in class fields, anything that needs to surviveawait.ArrayPool<T>— large or variable-size heap buffers that should be reused across calls.stackalloc— tiny fixed-size scratch space (< ~512 bytes) where you want zero heap allocation.