Updated on

Use Span<T>. It is faster, it never allocates, and it is the right parameter type for any synchronous method that reads or writes a buffer.

Reach for Memory<T> only where Span<T> cannot go. Span<T> is a ref struct, so it cannot be a field in a class, cannot be captured in a lambda, and cannot live across an await. Microsoft’s own Memory<T> and Span<T> usage guidelines give the reason in one line: “Because these types can only be stored on the stack, they’re unsuitable for scenarios such as asynchronous method calls.” Those three restrictions, not performance, are the entire reason Memory<T> exists.

To download the source code for this article, you can visit our GitHub repository.

What Is Span<T> in C#?

Span<T> is a type that represents a contiguous block of memory that already exists somewhere else. It is a view, not a container: creating one copies nothing and allocates nothing.

That block can live in three different places, and Span<T> does not care which: a managed array on the heap, a stackalloc buffer on the stack, or unmanaged memory behind a pointer. The same method signature accepts all three.

The point of it is slicing without copying. text.AsSpan(10, 5) gives us five characters to work with, where text.Substring(10, 5) gives us a new string and a heap allocation.

The cost of that is where it can live. Span<T> is declared as a ref struct, which the compiler keeps on the stack and refuses to let escape onto the heap, so it cannot be a field in a class, and cannot survive an await.

The Span<T> can have several sources for a region of memory:

  • an array T[] (or slice of an array)
  • Memory<T>
  • an unmanaged pointer
  • stackalloc

The Span<T> is ref struct type. As such, .NET always allocates it on the stack. It stores only the pointer for the already allocated reference type and doesn’t allocate any newly managed heap memory. Span<T> can’t be boxed or assigned to Object, dynamic, or interface type variables. It also can’t be a field in a reference type.

Using the Span<T> types don’t require the computation of the beginning of the pointer to a reference type and the offset, as this information is already contained in the Span<T>. That makes computations with it very fast. As the Span<T> doesn’t allocate any additional heap memory, the garbage collector works faster, making the entire application more performant.

When a buffer is only ever read, ReadOnlySpan<T> is the form to reach for. It carries the same view of memory but stops a caller from writing through it, which is why converting a string to a Span hands us a ReadOnlySpan<char> rather than a writable one.

To learn more about Span<T>, read How to Use Span in C# to Improve Application Performance.

What Is Memory<T> in C#?

Similarly to Span<T>, Memory<T> also represents a contiguous region of memory. But unlike Span<T>, Memory<T> is a struct:

public readonly struct Memory<T>
{
  private readonly object _object;
  private readonly int _index;
  private readonly int _length;
  ...
}

Memory<T> can be placed on the managed heap but also on the stack, the same as Span<T>. Memory<T> can be used as a field in a class and across await and yield boundaries, bypassing some Span<T> limitations.

The Span property of Memory<T> returns Span type that enables using the Memory<T> as a Span within the scope of a method. In this sense, Memory<T> is sometimes called a span factory.

The read-only counterpart is ReadOnlyMemory<T>, which relates to Memory<T> exactly as ReadOnlySpan<T> relates to Span<T>, and its Span property hands back a ReadOnlySpan<T>. That is the type we receive from an API that lends us a buffer it still owns, and the one we start from when converting ReadOnlyMemory to a byte array.

To explore Memory<T> further, read Using Memory For Efficient Memory Management in C#.

Span vs Memory in C#: What Is the Difference?

The difference is one keyword: Span<T> is a readonly ref struct, Memory<T> is a readonly struct. Everything else follows from that.

Because Span<T> is a ref struct, the compiler guarantees it never reaches the heap. That guarantee is what makes it safe to point at a stackalloc buffer, and it is also what makes it useless as a class field: the field would outlive the stack frame the buffer sits in.

Memory<T> gives that guarantee up. It is a normal struct holding an object reference, an index, and a length, so it can be stored in a field, closed over by a lambda, and held across an await.

When we need to actually touch the data, we ask Memory<T> for a Span<T> through its Span property, inside the method that does the work. That is the pattern: store Memory<T>, operate on Span<T>.

Diagram contrasting Span of T, which stays on the stack, with Memory of T, which can live on the stack or heap and produces a Span through its Span property.

Let’s look at a simple benchmark in which we are performing the same operations using both types, measuring allocations with BenchmarkDotNet:

[MemoryDiagnoser]
public class SpanMemoryBenchmark
{
    private readonly int[] data = [1, 2, 3, 4, 5, 6];

    [Benchmark]
    public Memory<int> SliceAsMemory()
    {
        return data.AsMemory().Slice(2, 1);
    }

    [Benchmark]
    public Span<int> SliceAsSpan()
    {
        return data.AsSpan().Slice(2, 1);
    }
}

We are slicing a number from an integer array in both benchmarked methods. But in the SliceAsMemory method, we access the array as Memory, while in the SliceAsSpan method we access it as Span. Both methods return their slice on purpose: a benchmark that discards its result lets the JIT delete the work, and what we would be timing is an empty method.

Benchmark results are:

| Method        | Mean      | Error     | StdDev    | Median    | Allocated |
|-------------- |----------:|----------:|----------:|----------:|----------:|
| SliceAsMemory | 2.8710 ns | 0.1251 ns | 0.3116 ns | 2.8776 ns |         - |
| SliceAsSpan   | 0.5306 ns | 0.1595 ns | 0.4703 ns | 0.6818 ns |         - |

As expected, Span is more performant, and neither type allocates. With that in mind, let’s identify the cases when we should use Memory instead of Span.

When Should We Use Memory<T> Instead of Span<T>?

We use Memory<T> when the buffer has to outlive the method that received it, and Span<T> everywhere else. Microsoft’s guidelines put it as a numbered rule: “For a synchronous API, use Span<T> instead of Memory<T> as a parameter if possible”.

Three situations force Memory<T>. The first is an asynchronous method: a Span<T> cannot survive an await, so any signature that reads or writes a buffer asynchronously takes Memory<T>. The second is a field: a class that holds onto a buffer between calls must hold Memory<T>. The third is a lambda or a local function that captures the buffer, because capturing lifts the variable into a compiler-generated class.

If none of the three situations applies, Span<T> remains the right answer, and passing Memory<T> instead is a small, needless cost paid for nothing.

For buffers we only read, the read-only forms, ReadOnlySpan<T> and ReadOnlyMemory<T>, say so in the signature and stop a caller from writing through our view.

CriterionSpan<T>Memory<T>
Kindreadonly ref structreadonly struct
Where it can liveStack onlyStack or heap
Field in a classNoYes
Captured in a lambdaNoYes
Held across await or yieldNoYes
Allocation when created from an arrayNoneNone
Read-only counterpartReadOnlySpan<T>ReadOnlyMemory<T>
Get the other oneNot applicable.Span property
Sources it can wrapArray, stackalloc, unmanaged pointer, Memory<T>Array, MemoryPool<T>, IMemoryOwner<T>
Use it forSynchronous parameters and localsAsync APIs, fields, and buffers passed around

If our method has a Memory<T> parameter and returns void, we must not use this Memory<T> instance after the method execution is over. Similarly, if the method returns Task, we must not use the instance after the Task terminates. Let’s look at examples of such incorrect usages:

static void WriteToConsole(Memory<int> output)
{
    Console.Write(output.ToString());
}

static Task WriteToConsoleTask(Memory<int> output)
{
    Console.Write(output.ToString());
    return Task.CompletedTask;
}

static void IncorrectUsageVoid()
{
    int[] data = [1, 2, 3, 4, 5, 6];
    var memory = data.AsMemory();
    WriteToConsole(memory);
    memory.Slice(2, 1); //Incorrect usage
}

static async Task IncorrectUsageTask()
{
    int[] data = [1, 2, 3, 4, 5, 6];
    var memory = data.AsMemory();
    await WriteToConsoleTask(memory);
    memory.Slice(2, 1); //Incorrect usage
}

We implemented two methods, WriteToConsole and WriteToConsoleTask. Both of them accept the Memory<int> parameter. The first is returning void, and the second Task. Within the methods IncorrectUsageVoid and IncorrectUsageTask, we call these methods. But after the calls, we still incorrectly use the Memory<T> instance.

When the constructor has a Memory<T> parameter or our type has a settable property of Memory<T> type, instance methods of this class are assumed to be consumers of the Memory<T> instance:

public class Consumers
{
    public Memory<int> MemoryToWrite { get; set; }

    void WriteToConsole(Memory<int> output)
    {
        Console.Write(output.ToString());
    }

    void WriteToConsole()
    {
        Console.Write(MemoryToWrite.ToString());
    }

    void WriteToConsole(string output)
    {
        Console.Write(output);
    }
}

In the Consumers class, we pass the Memory<int> instance to the WriteToConsole(Memory<int> output) method, so it is a consumer of a Memory<T> instance. But both WriteToConsole() and WriteToConsole(string output) are consumers of the Memory<T> instance. This rule exists because property setters or equivalent methods are assumed to capture and persist their inputs, so instance methods on the same object may utilize the captured state.

When API has an IMemoryOwner<T> parameter, the instance is accepting its ownership. We must dispose of the instance or transfer the ownership to avoid memory leaks:

void MemoryOwnerParameter(IMemoryOwner<int> output)
{
    Console.Write(output.ToString());
    output.Dispose();
}

The MemoryOwnerParameter method accepts IMemoryOwner as a parameter. It must be disposed of after usage. The same ownership question comes up when we are renting buffers with ArrayPool, where whoever rents the array is the one who has to return it.

Conclusion

While both types significantly improve the application performance and provide memory safety while working with unmanaged resources, it is essential to understand their implications and limitations.

Span<T> and Memory<T> are designed to avoid copying memory or allocating more than necessary to the managed heap. They represent a view of the memory.

For fast, local calculations and to avoid allocating unnecessary memory, a better choice is Span. But when we need to pass it as an argument or utilize it in an asynchronous method, we have to use Memory, which doesn’t carry those limitations.

The described types are welcome additions to the .NET ecosystem and provide significant performance boosts in critical cases, but it is important to be mindful of the difference between Span and Memory. The same instinct pays off elsewhere in the framework, for example when we are reducing string allocations with StringPool.

Tested with .NET 10.0.10 and BenchmarkDotNet 0.13.12.