Span / Memory / ReadOnlySequence in C#

6/8/2023

There are many different memory types used in modern C# programs. The more common ones are Span<T> and Memory<T>. Occasionally there is also ReadOnlySequence<T>. What do these types do?

Span<T>

Span: Is a fast synchrnous accessor of contiguous memory. It is often used in performance-critical scenarios as there are no new allocations going on when handling with Span. It is a view of a memory slice that is already there. It is a ref struct and therefore has many limitations (like no async/await, yield keyword, ...). Span<T> is highly optimized by the compiler and runtime. I will not go into too much detail as I already wrote in detail about Span<T>: "Create a low allocation and faster StringBuilder - Span in Action".

Memory<T>

Memory: Is almost the same as Span but doesn't have the limitation of living on the stack. It can be moved to the heap. With that you can use it inside async/await or have it as field on your class.

async Task ProcessMemory()
{
    var[] numbers = new int[] { 1, 2, 3, 4, 5 };
    Memory<int> memory = numbers;

    await ProcessAsync(memory);
}

async Task ProcessAsync(Memory<int> memory)
{
    for (var i = 0; i < memory.Length; i++)
    {
        memory.Span[i] *= 2;
        await Task.Delay(100); //simulate async work
    }
}

You could not do the same with a Span<T> as it is a ref struct and therefore can't be used as field or parameter of an async method. Sometimes you hear that Memory<T> is less optimized than Span<T>. But it's inaccurate to say that Memory<T> is less optimized than Span<T> in .NET. They serve different purposes and have other capabilities. Span<T> is a stack-only type that can wrap a segment of any memory, whether it's an array, a portion of a string, or unmanaged memory, whereas Memory<T> is a heap-allocated type that can be used more widely, including across async calls.

One could argue that Span<T> can be slightly more efficient in some specific scenarios. Span<T> allows for higher performance when you want to operate on a small segment of a larger block of data without creating a new array. The reason is that Span<T> is a stack-only struct that doesn't create additional heap allocations when slicing, for instance.

In contrast, Memory<T> is designed for situations where you might need to slice and dice data but have to work in a context where you cannot use a stack-only type, such as in async methods or when needing to store the data slice for longer periods.

Memory<T> and Span<T> provide similar capabilities when accessing and slicing data, aiming to minimize unnecessary copying and GC pressure. However, they have different trade-offs in terms of when and where they can be used due to the stack-only nature of Span<T>. So it's not so much a matter of optimization as it is a matter of using the right tool for the right job, depending on the specific needs of your code. It's best to profile and test your code to determine which structure works best for your needs.

ReadOnlySpan<T> and ReadOnlyMemory<T>

There is also a readonly version of those types available. The main difference is, as you guessed, the are read only, meaning you can't change the values inside the memory. This is useful if you want to pass a memory slice to a method but don't want the method to change the values. The most prominent example is ReadOnlySpan<char> which is used in many string methods.

ReadOnlySpan<char> text = "  Hello World  ";
ReadOnlySpan<char> trimmed = text.Trim(); // "Hello World"

The code operates purely on the slice of the original string. It doesn't create a new string. This is a very efficient way of working with strings. I could create the span from the string because there is an implicit conversion from string to ReadOnlySpan<char>. This is a very common pattern in the .NET framework. Many methods accept ReadOnlySpan<T> as parameter and therefore can be used with string as well.

ReadOnlySequence<T>

A sequence is a linked list of one or more Memory objects. If you have multiple pieces of memory that you want to tread as "one", this is the data structure of your choice. As consumer of this API it feels like you are "only" handling one memory slice.

var array1 = new[] { 1, 2, 3 };
var array2 = new[] { 4, 5, 6 };

var segment1 = new Segment<int>(array1);
var segment2 = new Segment<int>(array2);
segment1.SetNext(segment2);
var sequence = new ReadOnlySequence<int>(segment1, 0, segment2, array2.Length);

foreach(var memory in sequence)
{
    // ...
}

public class Segment<T> : ReadOnlySequenceSegment<T>
{
    public Segment(ReadOnlyMemory<T> memory)
    {
        Memory = memory;
    }

    public void SetNext(Segment<T> next)
    {
        Next = next;
        next.RunningIndex = RunningIndex + Memory.Length;
    }
}

The API is not necessary easy to handle as we have to create our own ReadOnlySequenceSegment<T> and link them together. But it's very powerful and can be used in many scenarios. For example if you want to read a file from disk and want to process it in chunks. You can read the file in chunks and create a sequence of memory slices. The consumer of the API can then process the file as if it was one big memory slice.

## Overview Here is a small overview of your mental picture: Poster

Conclusion

There you got it - different memory types in C#.

ReadOnlySpan<char> and strings - How not to compare them

Many know that you can take ReadOnlySpan<char> objects when dealing with strings. They give you a direct way of operating on the underlying memory. Often times you can use them interchangeably, but there are scenarios where you really have to watch out what is going on.

This blog post will have a look at a major problem with ReadOnlySpan when used like a "regular" string.

Memory is complicated

This is a small story about how memory operates in your .NET application. Well not only .NET but how memory does or does not get allocated.

We will see how a 1 Gigabyte big array is only a few megabytes big to some extend. Furthermore I will discuss working set and committed memory.

Help my memory dump always shows me some exceptions!

I made a memory dump in my simplest console application and there are a bunch of exception instances around, what is going on? Let’s see in this blog post, why you see a few exception instances in your memory dump.

An error has occurred. This application may no longer respond until reloaded. Reload x