In this article, we’ll briefly explain the Span and Memory .NET types and mention their primary usage. Furthermore, we will explore the differences between Span and Memory in C# in depth and conclude with some general tips on when to use one type over another.

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

What Is Span<T>?

The Span<T> type is a value type of .NET. It represents access to a continuous region of memory. In other words, Span<T> is only a view into underlying memory and isn’t a way to allocate that memory. 

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

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!
  • 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. Furthermore, as the Span<T> doesn’t allocate any additional heap memory, the garbage collector works faster, making the entire application more performant. 

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

What Is Memory<T>?

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. 

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

Differences Between Span and Memory in C#

The most notable difference between the two types is that Span<T> is ref struct, whereas Memory<T> is struct. This limits Span<T> allocation to the stack, while Memory<T> can be allocated on both the stack and the heap. 

Due to stack allocation, Span<T> can’t be a type of field in a class. Overall, Span<T> can’t be used in places where it may become a field in class. That includes capturing Span<T> into lambdas or local variables in async methods or iterators. Additionally, Span<T> can’t be used as a generic argument for reference type constraints. 

These limitations do not apply to Memory<T>. But, despite the limitations of Span<T>, it is more performant, and we should use it whenever possible and not simply avoid its limitations by using Memory<T>

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

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

    [Benchmark]
    public void SliceAsMemory()
    {
       data.AsMemory().Slice(2, 1);
    }

    [Benchmark]
    public void SliceAsSpan()
    {
        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

Benchmark results are: 

| Method        | Mean      | Error     | StdDev    | Allocated |
|-------------- |----------:|----------:|----------:|----------:|
| SliceAsMemory | 0.2760 ns | 0.0160 ns | 0.0134 ns |         - |
| SliceAsSpan   | 0.0239 ns | 0.0235 ns | 0.0220 ns |         - |

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

Guidelines for Working With Memory<T> and Span<T>

Generally, we should use Span<T> as a parameter for synchronous API whenever possible. If a buffer should be read-only, we should use read-only variants of Span<T> and Memory<T>.

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. 

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.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!