What Span<T> and Memory<T> Solve
Span<T> is a ref struct that represents a contiguous, type-safe view over a region of memory, whether that memory lives in a managed array, a stack-allocated buffer, or unmanaged memory. Before Span<T>, slicing a substring or array segment usually meant allocating a new copy (string.Substring, Array.Copy), which piles up garbage in hot paths like parsers or network protocol handlers. Because Span<T> is a ref struct, it can only live on the stack, which lets the runtime guarantee memory safety without needing GC tracking for the span itself, while still letting you index into the underlying buffer with bounds checking and zero additional allocation.
Cricket analogy: It's like a coach reviewing a specific over of a match on the broadcast feed without re-recording that footage into a new file; Span<T> lets you 'view' a slice of a buffer the same way, without copying the underlying data.
Memory<T> for Heap-Friendly, Async-Friendly Scenarios
Because Span<T> is a ref struct, it cannot be stored in a field of a class, boxed, used as a generic type argument, or captured in an async method's state machine, since those all require heap storage or crossing await boundaries. Memory<T> exists for exactly those cases: it is a regular struct wrapping a reference to an array, a MemoryManager<T>, or similar, and it can be stored on the heap, passed across await points, and converted to a Span<T> on demand via its .Span property when you actually need to read or write. A typical pattern is exposing a Memory<byte> from an async Stream.ReadAsync overload, then calling .Span inside a synchronous helper once the data has arrived.
Cricket analogy: It's like the difference between watching live action pitch-side (Span, only valid in the moment) versus a match highlights package you can store and replay anytime later (Memory), which you convert back to 'live view' when you're ready to analyze it.
Stack Allocation with stackalloc and ref structs
Span<T> pairs naturally with stackalloc, which allocates a fixed-size block directly on the stack instead of the managed heap, avoiding GC pressure entirely for small, short-lived buffers such as parsing a handful of bytes from a socket header. Because stack memory is reclaimed automatically when the method returns, and Span<T>'s ref-struct nature prevents it from escaping that method's lifetime (you cannot return a Span<T> wrapping a stackalloc'd buffer from the method that created it, and the compiler enforces this via lifetime/escape analysis), the combination is both fast and provably safe. This is why System.Text.Json and high-performance HTTP parsers in Kestrel lean heavily on Span<T> and stackalloc for zero-allocation parsing.
Cricket analogy: It's like a bowler chalking a temporary run-up mark on the pitch before an over that gets swept away by ground staff right after; stackalloc memory exists only for the duration of the method, just like that chalk mark only lasts the over.
// Zero-allocation parsing using Span<T> and stackalloc
public static bool TryParseHeader(ReadOnlySpan<byte> packet, out int messageLength)
{
// Slice without copying: a view into the original buffer
ReadOnlySpan<byte> lengthField = packet.Slice(0, 4);
messageLength = BinaryPrimitives.ReadInt32BigEndian(lengthField);
return messageLength > 0 && messageLength <= packet.Length - 4;
}
public static async Task<byte[]> ReadFrameAsync(Stream stream)
{
// Memory<T> can cross the await boundary; Span<T> cannot.
Memory<byte> buffer = new byte[4096];
int read = await stream.ReadAsync(buffer);
Span<byte> received = buffer.Span.Slice(0, read);
Span<byte> scratch = stackalloc byte[16]; // stack-only, zero GC pressure
received.Slice(0, 16).CopyTo(scratch);
return received.ToArray();
}Never store a Span<T> as a field on a class, use it as a generic type parameter, or try to keep it alive across an await. The compiler will reject most of these outright because Span<T> is a ref struct, but the underlying reason matters: it exists purely as a short-lived, stack-bound view, and Memory<T> is the tool for anything longer-lived.
System.Text.Json's Utf8JsonReader is built entirely around ReadOnlySpan<byte>, which is a major reason it dramatically outperforms Newtonsoft.Json's string-based parsing on large payloads: it never allocates copies just to tokenize input.
- Span<T> is a ref struct providing a stack-only, allocation-free, type-safe view over contiguous memory.
- Slicing a Span<T> or ReadOnlySpan<T> does not copy the underlying data, unlike string.Substring or Array.Copy.
- Memory<T> is a heap-friendly counterpart that can be stored in fields and crossed over await boundaries; call .Span to get a Span<T> when you need to read or write.
- stackalloc allocates a fixed-size buffer directly on the stack, avoiding GC pressure for small, short-lived data.
- The compiler enforces that a Span<T> can never outlive the stack frame that created it, preventing dangling references.
- High-performance libraries like System.Text.Json and ASP.NET Core Kestrel rely heavily on Span<T> for zero-allocation parsing.
- Use Memory<T> for async APIs and long-lived storage; use Span<T> for synchronous, in-method processing.
Practice what you learned
1. Why can't a Span<T> be stored as a field on a regular class?
2. What is the main reason Memory<T> exists alongside Span<T>?
3. What does slicing a Span<T> (e.g., span.Slice(2, 5)) do to the underlying data?
4. What is the benefit of combining stackalloc with Span<T>?
5. Which library is a well-known real-world example of using ReadOnlySpan<byte> extensively to avoid allocations during parsing?
Was this page helpful?
You May Also Like
Garbage Collection in .NET
How the .NET runtime automatically tracks and reclaims managed memory using a generational, tracing garbage collector, and how to work with it instead of against it.
Benchmarking with BenchmarkDotNet
Why naive stopwatch timing misleads you, and how BenchmarkDotNet produces statistically rigorous, allocation-aware performance measurements for .NET code.
async/await Fundamentals
How async/await really works under the hood in .NET, the differences between Task, Task<T>, and ValueTask<T>, and how to avoid the classic deadlock trap.
Related Reading
Related Study Notes in Microsoft Technologies
Browse all study notesWindows 10 / UWP Development Study Notes
.NET · 30 topics
Microsoft TechnologiesWindows Batch Scripting Study Notes
Batch · 30 topics
Microsoft TechnologiesMFC (Microsoft Foundation Classes) Study Notes
C++ · 30 topics
Microsoft TechnologiesSilverlight Study Notes
.NET · 30 topics
Microsoft TechnologiesXAML Study Notes
.NET · 30 topics
Microsoft TechnologiesWPF (Windows Presentation Foundation) Study Notes
.NET · 30 topics