DEV Community

Fabrizio BagalĂ 
Fabrizio BagalĂ 

Posted on • Updated on

Span, Memory, ReadOnlySequence in .NET

🔄 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, 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>.

var array = new byte[100];
var span1 = new Span<byte>(array);
var span2 = array.AsSpan();
Enter fullscreen mode Exit fullscreen mode

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.

var array = new byte[100];
var memory1 = new Memory<byte>(array);
var memory2 = array.AsMemory();
Enter fullscreen mode Exit fullscreen mode

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.

var array = new byte[100];
var sequence = new ReadOnlySequence<byte>(array);
Enter fullscreen mode Exit fullscreen mode

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. In this way, we can compare the performance of Span<T>, Memory<T>, and ReadOnlySequence<T> to arrays and lists.

[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);
    }
}
Enter fullscreen mode Exit fullscreen mode

The following results emerged from the comparison:

|                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 |
Enter fullscreen mode Exit fullscreen mode

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

Top comments (4)

Collapse
 
ant_f_dev profile image
Anthony Fung

Thanks for sharing. I'm quite surprised by the difference.

I'm also surprised that List outperformed Array. Do you have any ideas why this might be?

Collapse
 
fabriziobagala profile image
Fabrizio BagalĂ  • Edited

Span<T>, Memory<T> and ReadOnlySequence<T> are highly optimized structures so it is normal that they offer superior performance to traditional data structures.

Instead, the fact that the list has slightly higher performance is a problem due to the ToArray() method. I reran other tests using in the ArrayRange operation the ToCharArray() method:

[Benchmark(Baseline = true)]
public void ArrayRange()
{
    _ = _text.ToCharArray()[4..];
}
Enter fullscreen mode Exit fullscreen mode

And I got these results:

|                Method |        Mean |     Error |    StdDev |         Ratio | RatioSD |
|---------------------- |------------:|----------:|----------:|--------------:|--------:|
|            ArrayRange |  13.2293 ns | 0.0689 ns | 0.0611 ns |      baseline |         |
|             ListRange | 102.2282 ns | 0.6126 ns | 0.5730 ns |  7.72x slower |   0.06x |
|             SpanSlice |   0.5859 ns | 0.0010 ns | 0.0009 ns | 22.58x faster |   0.12x |
|           MemorySlice |   0.9230 ns | 0.0042 ns | 0.0039 ns | 14.33x faster |   0.05x |
| ReadOnlySequenceSlice |   9.1779 ns | 0.0263 ns | 0.0233 ns |  1.44x faster |   0.01x |
Enter fullscreen mode Exit fullscreen mode

I think the latter are the most realistic results.

Collapse
 
fabriziobagala profile image
Fabrizio BagalĂ 

@ant_f_dev I revised the benchmarks and gave a better explanation of the results.

Collapse
 
ant_f_dev profile image
Anthony Fung

Thanks for that.

The initial results had me wondering whether the only point of arrays was to indicate semantics (as it seemed they were the least performant of all the tested collections). The updated results seem more in line with what I thought might be the case with List vs Array.