đ 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();
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
andyield
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();
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);
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);
}
}
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 |
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.
Top comments (4)
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?
Span<T>
,Memory<T>
andReadOnlySequence<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 theArrayRange
operation theToCharArray()
method:And I got these results:
I think the latter are the most realistic results.
@ant_f_dev I revised the benchmarks and gave a better explanation of the results.
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.