Span, Memory, ReadOnlySequence in .NET
Tags: programming, coding, csharp, dotnet
Published: 2023-06-09 17:05:43

> 🔄 **Updates**
> _June 10, 2023 - Fixed benchmark._

In the landscape of the .NET, a key aspect is effective data manipulation. This capability is even more crucial when it comes to memory management. The `Span<T>`, `Memory<T>`, and `ReadOnlySequence<T>` structures provide advanced tools for optimal data management. These structures not only provide a high degree of control and flexibility, but also greatly increase the efficiency of operations.

## Span

> _Provides a type-safe and memory-safe representation of a contiguous region of arbitrary memory._

`Span<T>` is a [ref struct](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/ref-struct), and as such, it is allocated on the stack, not on the managed heap. This specific attribute imposes certain restrictions to ensure they do not get promoted to the managed heap. The constraints include preventing them from being boxed, assigned to variables of type `Object` or `dynamic` or any interface type, being fields in a reference type, and their use across await and yield boundaries. Invoking methods like `Equals(Object)` and `GetHashCode` on `Span<T>` will throw a `NotSupportedException`.

Given its stack-only nature, `Span<T>` might not be appropriate for scenarios that necessitate storing references to buffers on the heap. This restriction often applies in routines making asynchronous method calls. In these cases, complementary types like `System.Memory<T>` and `System.ReadOnlyMemory<T>` should be considered.

When dealing with immutable or read-only structures, it's more appropriate to use `System.ReadOnlySpan<T>`.

```csharp
var array = new byte[100];
var span1 = new Span<byte>(array);
var span2 = array.AsSpan();
```

In this example, a `byte` array of 100 elements is first created. Then, two instances of `Span<byte>` are then constructed from this array using two distinct methods. The first instance is created directly via the `Span<T>` constructor, and the second one uses the `AsSpan()` extension method available on arrays. The use of the `AsSpan()` method can often lead to more readable and concise code, especially when performing additional operations on the span inline.

## Memory

> _Represents a contiguous region of memory._

Similar to `Span<T>`, `Memory<T>` denotes a contiguous memory region. However, a key difference lies in their structure: `Memory<T>` is not a `ref struct`, unlike `Span<T>`. This structural distinction means that `Memory<T>` can reside on the managed heap, a feature not shared with `Span<T>`. As a result, `Memory<T>` is not bound by the same constraints as a `Span<T>` instance. Specifically:

- It can be utilized as a field in a class.
- It can operate across `await` and `yield` boundaries.

Alongside `Memory<T>`, `System.ReadOnlyMemory<T>` can be used to signify immutable or read-only memory, making it a versatile tool for handling different data storage needs.

```csharp
var array = new byte[100];
var memory1 = new Memory<byte>(array);
var memory2 = array.AsMemory();
```

In this instance, mirroring the previous example, we first create a `byte` array of 100 elements and then two instances of `Memory<T>` from this array. The first instance is created via the constructor, the second via the readable and concise `AsMemory()` extension method.

## ReadOnlySequence

> _Represents a sequence that can read a sequential series of `T`._

`ReadOnlySequence<T>` is a structure specifically engineered to manage non-contiguous data sequences, offering substantial utility when dealing with data scattered across multiple arrays or buffers. Essentially, it's a sequence representing a linked list of one or more `Memory` objects. If you find yourself needing to treat multiple memory fragments as a single entity, this data structure is your best option.

```csharp
var array = new byte[100];
var sequence = new ReadOnlySequence<byte>(array);
```

In this code snippet, a `byte` array is created, and a `ReadOnlySequence<byte>` is then constructed from this array. The resulting `ReadOnlySequence<byte>` provides a unified view over the byte array as if it were a single, contiguous slice.

## Benchmark

To objectively assess the performance of these structures, we can use a benchmarking framework like [BenchmarkDotNet](https://benchmarkdotnet.org/). In this way, we can compare the performance of `Span<T>`, `Memory<T>`, and `ReadOnlySequence<T>` to arrays and lists.

```csharp
[Config(typeof(MemoryBenchmarkConfig))]
public class MemoryBenchmark
{
    private sealed class MemoryBenchmarkConfig : ManualConfig
    {
        public MemoryBenchmarkConfig()
        {
            SummaryStyle = BenchmarkDotNet.Reports.SummaryStyle.Default.WithRatioStyle(RatioStyle.Trend);
        }
    }
    
    private const int OperationsPerInvoke = 4096;
    
    private string _text = null!;
    
    [GlobalSetup]
    public void Setup()
    {
        _text = "Hello World";
    }

    [Benchmark(Baseline = true)]
    public void ArrayRange()
    {
        _ = _text[4..];
    }
    
    [Benchmark]
    public void ListRange()
    {
        _ = _text.ToList().GetRange(4, 7);
    }
    
    [Benchmark(OperationsPerInvoke = OperationsPerInvoke)]
    public void SpanSlice()
    {
        for (var i = 0; i < OperationsPerInvoke; i++)
        {
            _ = _text.AsSpan().Slice(4);
        }
    }
    
    [Benchmark]
    public void MemorySlice()
    {
        _ = _text.AsMemory().Slice(4);
    }
    
    [Benchmark]
    public void ReadOnlySequenceSlice()
    {
        _ = new ReadOnlySequence<char>(_text.AsMemory()).Slice(4);
    }
}
```

The following results emerged from the comparison:

```markdown
|                Method |        Mean |     Error |    StdDev |         Ratio | RatioSD |
|---------------------- |------------:|----------:|----------:|--------------:|--------:|
|            ArrayRange |   6.1415 ns | 0.0235 ns | 0.0220 ns |      baseline |         |
|             ListRange | 102.9986 ns | 0.3432 ns | 0.3210 ns | 16.77x slower |   0.09x |
|             SpanSlice |   0.5854 ns | 0.0009 ns | 0.0008 ns | 10.49x faster |   0.04x |
|           MemorySlice |   0.8952 ns | 0.0029 ns | 0.0027 ns |  6.86x faster |   0.03x |
| ReadOnlySequenceSlice |   9.1725 ns | 0.0198 ns | 0.0185 ns |  1.49x slower |   0.01x |
```

In the table, `ArrayRange` serves as a reference point, given its essential and direct nature.

`ListRange` performs much less well than baseline. This is probably due to the inherent complexity of lists, which, while offering great flexibility, require a higher overhead than arrays.

In contrast, `SpanSlice` and `MemorySlice` far outperform `ArrayRange` in terms of speed. This suggests that their ability to operate on portions of data rather than entire collections allows for more efficient and faster data manipulation.

Finally, `ReadOnlySequenceSlice` ranks slightly below `ArrayRange` in terms of performance. This may be because `ReadOnlySequenceSlice` offers advantages in terms of data security, as it prevents unwanted modifications. However, these advantages come at a small cost in terms of performance.

> ⚠️ **Warning**
> _Keep in mind that you may get different values than those given in the table. This depends on various factors, such as the length and complexity of the input strings, the hardware of the machine on which the code is running, etc._

## Conclusion

The advanced memory structures provided by .NET, like `Span<T>`, `Memory<T>`, and `ReadOnlySequence<T>`, can deliver significant efficiency and performance benefits when dealing with contiguous or disconnected data. Familiarity with these structures and the ability to effectively utilize them can be a core skill for .NET developers aiming to optimize their applications.

## References

- [Span<T> Struct](https://learn.microsoft.com/en-us/dotnet/api/system.span-1?view=net-6.0)
- [Memory<T> Struct](https://learn.microsoft.com/en-us/dotnet/api/system.memory-1?view=net-6.0)
- [ReadOnlySequence<T> Struct](https://learn.microsoft.com/en-us/dotnet/api/system.buffers.readonlysequence-1?view=net-6.0)