Memory vs Span in C#: Key Differences Explained
Modern .NET applications frequently process large buffers of
contiguous memory, requiring high performance without unnecessary
allocations. Both Span<T> and
Memory<T> were introduced to solve this challenge by
providing type-safe, contiguous views of arbitrary memory regions, such
as managed arrays, native buffers, or stack-allocated memory. The
primary difference between them lies in how and where they can be
allocated: Span<T> is a stack-only structure
optimized for synchronous, ultra-low-overhead slicing, whereas
Memory<T> can live on the managed heap, making it
compatible with asynchronous programming patterns and long-lived
storage.
Understanding Span<T>
Span<T> is declared as a ref struct,
a special constraint enforced by the C# compiler ensuring that instances
always reside strictly on the execution stack. Because it lives
exclusively on the stack, it eliminates garbage collection (GC) overhead
and completely prevents issues related to tracking pointers across heap
objects.
A Span<T> provides direct, pointer-like access to
contiguous blocks of memory. It can wrap:
- Managed arrays (
T[]) - Stack-allocated memory (
stackalloc T[]) - Unmanaged, native memory allocated via native interop
Because of its ref struct nature,
Span<T> has distinct limitations:
- It cannot be boxed or cast to an interface.
- It cannot be used as a generic type argument.
- It cannot be a field in a regular class or non-ref struct.
- It cannot cross
awaitboundaries inasyncmethods, because state machines generated by the compiler are stored on the heap.
Understanding Memory<T>
Memory<T> is a standard, non-ref
readonly struct. It was designed specifically to lift the
stack restrictions of Span<T> while retaining the
capability to represent contiguous memory safely.
Because Memory<T> is an ordinary struct, it can be
allocated on the heap. This allows it to:
- Be stored as a field within standard classes.
- Cross
awaitboundaries inside asynchronous methods. - Be placed inside collections and captured by lambda expressions.
However, Memory<T> does not provide direct element
indexing. To read, write, or slice individual elements, you access its
underlying Span property by calling
memory.Span. This produces a transient
Span<T> over the data, which can then be read or
modified with zero overhead on the stack.
Core Differences
| Feature | Span<T> |
Memory<T> |
|---|---|---|
| Type Definition | ref struct |
Standard readonly struct |
| Storage Location | Stack only | Stack or Heap |
| Async Support | Cannot cross await
points |
Supported across await
points |
| Element Access | Direct indexing
(span[i]) |
Indirect via .Span
property |
| Use as Class Field | Not allowed | Allowed |
| Heap Overhead | Zero | Zero (unless boxed) |
When to Choose Between Them
Choose Span<T> when writing
synchronous, performance-critical code where the memory scope does not
leave the current stack frame. It is the ideal choice for parsing text,
transforming arrays, and handling byte slices inside synchronous
algorithms.
Choose Memory<T> when you are
building asynchronous pipelines, such as reading from a network stream
with
Stream.ReadAsync(Memory<byte>, CancellationToken),
storing buffers in long-lived stateful classes, or passing data into
background tasks. In these scenarios, Memory<T>
maintains the buffer reference on the heap, and you obtain its
Span<T> only within the synchronous segments of
execution where the actual data processing occurs.