C# Memory vs Span: Choosing the Right Approach
c# memory vs span: Understand the differences between Memory<T> and Span<T> in C#, including when to use each, their runtime behavior, and practical guidance for high-...
c# memory vs span requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
When working with high-performance C# code, you will eventually face the decision: Memory<T> or Span<T>? Both types provide a view over contiguous memory, but they are fundamentally different in their capabilities and intended usage. If you misuse them, you may hit compilation errors that seem frustratingly arbitrary. This article clarifies the core distinction and provides practical guidance on when to use each type.
The Core Distinction: Stack vs Heap
The fundamental difference between Span<T> and Memory<T> lies in where they can live. Span<T> is a ref struct, which means it can only exist on the stack. It cannot be a field of a class, cannot be used in async methods, and cannot be boxed. Memory<T>, on the other hand, is a regular struct that can live on the heap, be stored in fields, and used across async boundaries.
This constraint is not a limitation invented by the compiler. It's a safety mechanism. A Span<T> often points to stack-allocated memory or unmanaged memory. If a Span<T> were allowed to escape to the heap, it could outlive the memory it refers to, leading to dangling references and memory corruption. The compiler enforces the ref struct restriction to guarantee that Span<T> cannot outlive its underlying data.
Here's a minimal example to illustrate the compile-time error you'll get when trying to use Span<T> in a class field:
public class Example { // This line will not compile: // public Span<int> _span; // Error: Span<int> cannot be used as a field }
The error message states something like: Span<T> cannot be used as a field. This is because a class instance lives on the heap, and the Span<T> inside it could be accessed after the stack frame that created it has unwound.
Memory<T> does not have this restriction. It can be stored in a class, used in a lambda, or passed to an async method.
public class Example { public Memory<int> _memory; }
The reason Memory<T> is safe is that it does not hold a direct pointer to the data. Instead, it holds a reference to a managed object (like an array) and an index/length. This reference keeps the managed object alive, preventing memory corruption.
When to Use Span<T>
Span<T> is the go-to choice for synchronous, CPU-bound operations where performance is critical. Because it can point to stack memory or unmanaged memory without any extra allocation overhead, it excels in scenarios like parsing or processing buffered data.
A common pattern is using Span<T> to avoid allocating substrings or arrays when parsing text. Consider processing a comma-separated value (CSV) line without allocating strings for each field:
using System; using System.Buffers; public static void ProcessCsvLine(string line) { ReadOnlySpan<char> span = line.AsSpan(); while (span.Length > 0) { int commaIndex = span.IndexOf(','); ReadOnlySpan<char> field; if (commaIndex == -1) { field = span; span = default; } else { field = span.Slice(0, commaIndex); span = span.Slice(commaIndex + 1); } // process the field without allocation Console.WriteLine(field.ToString()); // only for demonstration; normally you'd avoid ToString() } }
In this example, Span<T> allows you to slice the string without creating any new string allocations. The temporary slices are stack-allocated, making the operation highly efficient. Without Span<T>, you would need to call Substring, which allocates a new string for each field, increasing GC pressure.
Another powerful use of Span<T> is with stackalloc. You can create a small buffer on the stack without hitting the heap.
Span<byte> buffer = stackalloc byte[256];
This is extremely fast because stack allocation is just a pointer increment, but it's only valid within the current method's scope. The compiler will not let you return this Span<byte> from the method.
When to Use Memory<T>
Memory<T> is designed for scenarios that require the data to be stored or processed asynchronously. Because it can live on the heap, you can use it in async methods, which is forbidden for Span<T>. A typical usage is buffering data read from a network stream.
Consider a class that reads data asynchronously and passes the buffer to a processing method. The Memory<T> allows the buffer to be stored as a field and passed to methods that return Task.
using System; using System.IO; using System.Threading.Tasks; public class StreamProcessor { private readonly Memory<byte> _buffer = new byte[4096]; public async Task ProcessAsync(Stream stream) { while (true) { int read = await stream.ReadAsync(_buffer); if (read == 0) break; // Process the data synchronously using Span<T> ProcessChunk(_buffer.Span.Slice(0, read)); } } private void ProcessChunk(ReadOnlySpan<byte> chunk) { // Synchronous processing here } }
In this example, Memory<byte> serves as the asynchronous buffer. The ReadAsync method requires a Memory<byte> parameter. Once the data is read, you get a Span<T> via the .Span property to perform synchronous processing. This way, you combine the benefits of both types.
Another common use is storing Memory<T> in a reusable buffer pool. During a single request, you may need to hold a buffer while awaiting a database query. Memory<T> allows you to keep the buffer in a local variable across awaits.
Passing Between Methods: API Design
When you design a library or a public API, you need to decide which type to expose. The rule of thumb is to take Span<T> when your method is synchronous and doesn't store the data. If your method is async, or needs to store the data beyond the method's lifetime, use Memory<T>.
Consider a method that processes a block of data and returns a result. If the processing is synchronous, you can accept ReadOnlySpan<T> to give maximum flexibility to the caller. The caller can pass a slice of an array, a stackalloc buffer, or even a pointer to unmanaged memory.
public static int CalculateSum(ReadOnlySpan<int> numbers) { int sum = 0; foreach (var n in numbers) sum += n; return sum; }
This method can be called with an array, a slice of an array, or a stackalloc buffer. If you accepted Memory<int> instead, the caller would have to allocate an array or use a Memory instance, losing the flexibility of stack allocation.
When returning data that will be consumed later, you should return Memory<T> only if the data is held by a managed object such as an array. Returning a Span<T> from a method is only allowed if the span points to stackalloc memory or if the method is ref returning a field. In general, it's safer to design APIs to return arrays or Memory<T>.
Performance Implications
Performance is a significant reason to choose between Span<T> and Memory<T>. Span<T> provides direct pointer access with bounds checking, which is very fast. Accessing a Span<T> index is essentially a pointer dereference plus a bounds check, similar to an array access.
Memory<T> adds a small overhead because accessing its .Span property involves a virtual call or a conditional check to retrieve the underlying object reference and offset. In tight loops, this overhead can be noticeable, but it is usually negligible in I/O-bound scenarios.
A crucial difference is allocation. A Span<T> itself is a small struct that contains a pointer and a length. It does not allocate on the heap. Memory<T> is also a struct, but it internally holds an object reference, so it may prevent the GC from collecting an array until it's no longer referenced. Using a Memory<T> as a field in a long-lived object means the underlying array cannot be collected, which could increase memory usage.
For maximum performance in CPU-bound loops, prefer operating on Span<T> directly and avoid repeated .Span property access. For example:
// Good: Accessing Span once var span = memory.Span; for (int i = 0; i < span.Length; i++) { span[i]++; }
Avoid accessing memory.Span inside a loop, as it introduces repeated virtual call overhead.
Common Pitfalls and How to Avoid Them
One frequent mistake is attempting to use Span<T> in an async method. The compiler will reject it because the Span<T> could survive across an await. Instead, use Memory<T> and convert to Span<T> only after the await completes.
Another trap is storing a Span<T> in a ref struct that is then stored in a field of a class. That is illegal for the same reason. You can only store ref structs in other ref structs.
Also, be careful when creating a Span<T> from an array that is frequently resized. If the array is reallocated, the Span<T> still points to the old array, which can cause stale data and memory waste. Span<T> does not track the array's length changes.
The Relationship Between Memory<T> and Span<T>
Memory<T> has a .Span property that returns a Span<T> for the entire memory. This conversion is straightforward:
Memory<int> memory = new int[10]; Span<int> span = memory.Span;
Conversely, you cannot directly convert a Span<T> to a Memory<T> because the span might be pointing to stack memory that would not be safe to hold. However, you can create a Memory<T> from an array, which is a common safe scenario.
This relationship means that you often write methods that accept Memory<T> but internally create a Span<T> to do fast processing. The conversion from Memory<T> to Span<T> is cheap and does not allocate.
How the Compiler Enforces Safety
The compiler uses the ref struct annotation to enforce the stack-only rule. Any type that is a ref struct has the same restrictions. This is a compile-time constraint, so you get an error before your code even runs. Understanding these restrictions is essential to avoid compile errors that block development.
For example, you cannot box a ref struct implicitly. This prevents you from storing a Span<T> in a List<object> or using it as a type argument for a generic type that may allocate. Once you understand the rule, you can avoid these pitfalls by switching to Memory<T> when you need heap storage.
When to Use Each: A Decision Guide
| Scenario | Use Span<T> | Use Memory<T> |
|---|---|---|
| Synchronous CPU-bound processing | Yes | No (unnecessary overhead) |
Over async boundaries | No (compile error) | Yes |
| Store in a class field | No | Yes |
| Use in iterators (yield) | No | Yes |
| Point to stack-allocated memory | Yes | No |
| Point to unmanaged memory | Yes | No |
This table summarizes the main decision criteria. In short, if you are doing a quick, synchronous operation and don't need to store the data, use Span<T>. If you need the data to be stored or processed asynchronously, use Memory<T>.
A Practical Example: A Buffer Pool
A common pattern in high-scale servers is to use a buffer pool to reduce allocations. Memory<T> fits perfectly here because you can rent a buffer and hold it as Memory<T> while you process it across awaits.
using System.Buffers; using System.Threading.Tasks; using System.IO; public class AsyncProcessor { private const int BufferSize = 1024; public async Task ProcessAsync(Stream stream) { Memory<byte> buffer = new byte[BufferSize]; // In practice, you'd use ArrayPool<byte>.Shared.Rent() var rented = MemoryPool<byte>.Shared.Rent(BufferSize); Memory<byte> mem = rented.Memory; try { while (true) { int read = await stream.ReadAsync(mem); if (read == 0) break; ProcessData(mem.Span.Slice(0, read)); } } finally { rented.Dispose(); } } private void ProcessData(ReadOnlySpan<byte> data) { // Fast processing } }
This example shows the typical workflow: rent a memory block, use it asynchronously, then process a span of the data synchronously. The memory is returned to the pool when done, reducing GC pressure.
Note that MemoryPool<T> and ArrayPool<T> are built-in .NET features that allow reusable buffers. This pattern is widely used in networking and file processing where buffering is frequent.
Compatibility and Future Directions
Span<T> and Memory<T> are deeply integrated into modern .NET (Core 2.1 and later, including .NET 5+). If you are targeting .NET Framework, you can use them if you install the System.Memory NuGet package, but not all APIs are optimized for them. In modern .NET, many BCL methods have overloads accepting ReadOnlySpan<T>, so using Span<T> enables you to call these optimized methods, avoiding allocations.
The language design is mature, and the restrictions are unlikely to change. So it's essential to learn these patterns to write efficient code in modern C#.
Advanced Scenario: Using Span<T> with Stackalloc
An advanced usage is to combine stackalloc with Span<T> to avoid heap allocation for small temporary buffers. This is particularly useful in algorithms that require a small array of bytes or integers for temporary storage.
public static void ProcessBytes(ReadOnlySpan<byte> input) { Span<byte> temp = stackalloc byte[input.Length]; // careful: length must be small enough to avoid stack overflow // Fill temp and process }
The stackalloc memory is automatically freed when the method returns. This eliminates any garbage collection for temporary data. However, you must be careful not to allocate too much on the stack, as the stack has limited size (usually around 1MB). A good rule is to use stackalloc only for arrays smaller than a few hundred bytes.
If you need a larger temporary buffer, use ArrayPool<byte>.Shared.Rent() and get a Memory<T>.
Maintaining Code Clarity
Using Span<T> and Memory<T> can make your code more complex due to the restrictions. It's important to balance performance with maintainability. For most application-level code, using arrays and strings is perfectly fine. Reserve Span<T> and Memory<T> for performance-critical paths, such as parsers, network protocol implementations, or image processing.
When you do use them, keep the Span<T> confined to internal helper methods that are short and purely computational. Expose Memory<T> for any public API that needs to store or process data asynchronously. This way, you keep the performance benefits while keeping the code understandable.