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 await boundaries in async methods, 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 await boundaries 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.