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>
Spanasync
/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
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
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:
Conclusion
There you got it - different memory types in C#.