C# Span vs Memory: Choosing the Right Type
c# span vs memory: Learn when to use Span<T> and when to use Memory<T> in C#. Understand stack-only constraints, async limitations, streaming scenarios, and practical...
c# span vs memory requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
Choosing between Span<T> and Memory<T> is one of the more common design decisions when writing high-throughput C# code. Both types represent a contiguous region of memory, and both can wrap arrays, strings, or unmanaged memory. But they have different constraints and are suited to different parts of a pipeline.
The core distinction is that Span<T> is a ref struct and can only live on the stack, while Memory<T> is a regular struct and can live on the heap. This single difference drives nearly every practical decision between them. Once you understand what that means at runtime, the rest of the guidance follows naturally.
What Changes With a ref struct
A ref struct type is subject to compiler-enforced restrictions. You cannot box it, you cannot use it as a field in a class, and you cannot use it as a generic type argument. The compiler also prevents capturing a Span<T> in a lambda or an async method.
public void Process(Span<byte> buffer) { // This method is fine. } public async Task ProcessAsync(byte[] data) { // Span cannot be used across an await. Span<byte> buffer = data.AsSpan(); await Task.Delay(100); // The span is no longer accessible here. }
That restriction exists so that a span cannot outlive the stack frame where it was created. The runtime does not need to track or pin the underlying memory, because the span is always used within the synchronous scope of a method. This makes operations like slicing and iteration extremely cheap.
Memory<T> has none of those restrictions. It can be stored in a class, passed across async calls, or used as a generic parameter. The cost is that operations on Memory<T> are slightly heavier because the runtime may need to track the lifecycle of the underlying object.
Where Memory<T> Becomes Necessary
Async code is the most common reason to use Memory<T>. When you need to read from a stream, process the buffer, and then continue processing asynchronously, a span cannot survive the await boundary.
public async Task ReadAndWriteAsync(Stream source, Stream destination) { Memory<byte> buffer = new byte[8192]; int bytesRead = await source.ReadAsync(buffer); // Process the buffer. _ = destination.Write(buffer.Span); }
That code works because Memory<byte> can be stored in an async method's state machine. The buffer.Span property gives you a Span<byte> for the duration of the Write call, but you cannot keep that span across the next await.
Another scenario is storing a reference to a region of memory for later use. For example, a parser might read a chunk of data, extract a section of it, and pass that section to a worker thread. Memory<T> can be safely stored in a wrapper object, while Span<T> cannot.
The Trade-Off in Stack-Only Usage
When you are working synchronously and the lifetime of the buffer is confined to a single method, Span<T> is usually the better choice. It enables features that Memory<T> cannot offer, such as stackalloc.
Span<byte> stackBuffer = stackalloc byte[256]; Fill(stackBuffer); // Use stackBuffer solely within this method.
stackalloc allocates on the stack, avoiding heap allocation entirely. That is only possible with a stack-only type. Memory<T> cannot wrap a stackalloc buffer because the heap-allocated Memory<T> would outlive the stack frame.
For synchronous data manipulation, Span<T> also has a slight performance advantage because the runtime does not need to perform the same object tracking that Memory<T> requires. In practice, that difference is rarely the deciding factor unless you are processing many small buffers in a tight loop.
Choosing the Right Abstraction for Your API
When designing a public method that accepts or returns a buffer, you need to think about the constraints you want to place on callers.
If you accept Span<T> as a parameter, callers can pass arrays, strings, stackalloc buffers, or pointers. But they cannot pass a Memory<T> directly; they must get the span first, which requires that the memory be used synchronously.
If you accept Memory<T>, callers can pass arrays or pooled buffers, and the method can store the memory for later. The method cannot be called with a stackalloc buffer, because that would require converting a ref struct to a heap type, which the compiler prevents.
A common pattern in modern .NET libraries is to expose both overloads. The Memory<T> overload handles async scenarios, and the Span<T> overload provides a synchronous fast path that avoids the extra overhead of the Memory<T> wrapper.
Performance Implications Beyond the Wrapper
Beyond the stack-only restriction, the choice between Span<T> and Memory<T> affects how you can pin and pool memory. If you are working with unmanaged memory or need to pass a buffer to a native API, Span<T> offers a straightforward way to access that memory without copying.
IntPtr unmanagedPtr = Marshal.AllocHGlobal(1024); try { var span = new Span<byte>((byte*)unmanagedPtr, 1024); // Process the native buffer. } finally { Marshal.FreeHGlobal(unmanagedPtr); }
That code requires unsafe context, but it shows how a span can directly wrap a pointer. Memory<T> can also wrap unmanaged memory, but doing so requires a MemoryManager<T> implementation that defines how the memory is pinned and released. That added complexity usually only pays off when you need the buffer to survive across an async boundary.
When using ArrayPool<T>, the returned array is a regular managed array. You can pass it to a method that accepts Memory<T> without any extra wrapper, because T[] implicitly converts to Memory<T>. To pass that array to a Span<T>-based method, you call AsSpan() first.
byte[] rented = ArrayPool<byte>.Shared.Rent(1024); try { Memory<byte> memory = rented; ProcessBuffer(memory); // Works with Memory<T>. ProcessBuffer(rented.AsSpan()); // Works with Span<T>. } finally { ArrayPool<byte>.Shared.Return(rented); }
Those two calls are functionally similar, but they represent different contracts. ProcessBuffer(Memory<byte>) could store that buffer and use it after the method returns. ProcessBuffer(Span<byte>) cannot, because a span cannot outlive the current stack frame.
The difference matters when you are sending a pooled buffer to an async method. If the async method stores the Memory<T> and uses it after the await, you must ensure the buffer is not returned to the pool until the async operation completes. With a span, that risk is impossible because the span is not allowed to live that long.
Practical Decision Rules to Guide Your Code
The practical rule is to use Span<T> for synchronous, short-lived access to a buffer, and Memory<T> when the buffer must leave the current method or be processed asynchronously. The following situations push you toward Memory<T>:
- The buffer is stored in a class or a struct that lives on the heap.
- The buffer is used after an
awaitin an async method. - The buffer is passed to a lambda that is invoked asynchronously.
- The buffer is part of a generic type that requires a non-ref type.
Situations that favor Span<T> include:
- Synchronous validation, parsing, or transformation of a buffer.
- Working with
stackallocmemory. - Reading and writing segments of a string without allocation.
- Interfacing with unmanaged memory in an unsafe context.
- Writing a hot path where a
Memory<T>gets the span on every call.
There is no universal answer. The decision changes depending on whether you control the whole pipeline or you are designing an API for other developers. In a library, the safest approach is to offer both overloads when the underlying operation is synchronous, and only offer Memory<T> when async is required.
Compatibility and Reader Expectations
Span<T> and Memory<T> were introduced in .NET Core 2.1 and are available in .NET Standard 2.1 and later. If you are targeting older frameworks, you cannot use these types without adding a NuGet package that backports the APIs. This matters when you are building libraries that must run on .NET Framework or Unity.
When choosing between the two, you are also choosing the level of flexibility you offer to downstream consumers. A library that exposes only Memory<T>-based APIs makes it impossible for callers to pass a stackalloc buffer unless they copy it to the heap. A library that exposes only Span<T>-based APIs forces callers to either use it synchronously or convert the data to a heap-allocated structure before using it.
Modern .NET APIs in the base class library often provide both, so you can follow the same pattern. For example, Stream.Read(Span<byte>) calls Read(Memory<byte>) underneath, allowing both synchronous and asynchronous consumers to work with a single method. You can do the same by having a Memory<T>-based method call a Span<T>-based implementation, or the reverse, depending on where the heavier logic lives.
Handling Streaming and Partial Reads
When reading from a stream, you often get fewer bytes than you requested. Using Memory<T> lets you track the number of bytes read and pass that information to a processing method that accepts a span of the exact length.
Memory<byte> buffer = new byte[8192]; int bytesRead = await stream.ReadAsync(buffer); Memory<byte> used = buffer.Slice(0, bytesRead); // Now process only the used part. Process(used.Span);
That pattern avoids copying the unused part of the buffer. If you were using an array directly, you would need to pass the offset and count separately, or copy the used portion into a new array. The Memory<T> slice keeps the same underlying array, but gives you a smaller view of it.
The same slicing works with Span<T> inside a synchronous method, but the span version cannot cross an async boundary. So for a synchronous read from a NetworkStream that returns the number of bytes read, you can handle it entirely with span:
Span<byte> buffer = stackalloc byte[1024]; int bytesRead = stream.Read(buffer); Span<byte> used = buffer.Slice(0, bytesRead); // Process the used span.
That code works because Stream.Read(Span<byte>) is a synchronous API. The moment you switch to ReadAsync, you must switch to Memory<T> for the buffer itself. The span that you use to process the result should be obtained from the memory just before the processing call.
Final Note on API Design and Read-Only Views
A common mistake is to use Memory<T> for parameters that are only read, when ReadOnlyMemory<T> would be more accurate. The same distinction applies to Span<T> versus ReadOnlySpan<T>. Using the read-only variants communicates to the caller that you do not intend to modify the buffer, which can help avoid unnecessary copies or defensive cloning.
When you accept Memory<T>, you also accept the responsibility of ensuring the underlying buffer is not returned to a pool while the method is using it. This is especially important in async code, where the buffer may be used after the method returns. A span, by contrast, cannot outlive the stack frame, so the buffer lifetime is naturally scoped.
The decision between c# span vs memory is not about which type is faster in absolute terms. It is about where the buffer needs to live and how long it will be alive. Choosing the wrong type leads to either a compile-time error or a subtle runtime bug where a pooled buffer is used after it has been returned. The compiler will catch the span violation at the await boundary, but it will not catch the case where you store a Memory<T> and return the buffer to a pool too early. That is where careful lifetime management becomes essential.
For most synchronous, high-throughput code, Span<T> is the better default because it is more restrictive and therefore safer. For any code that touches async I/O or stores buffers for later processing, Memory<T> (or ReadOnlyMemory<T>) is the practical choice. Matching the type to the lifetime requirement keeps your code both fast and correct.