Saying that I disagree with the documentation of the new memory abstractions in .NET would be an overstatement. After working a bit with
IBufferWriter I model it in my head in a bit different way. Each of these terms has a one line definition that one could reason from. I hope that newcomers to this memory friendly world will think of it a good primer. At the same time, I hope it delivers a refreshing view for more experienced engineers. It does not aim to deliver the whole description. It aims to be a useful model as all models are wrong but some of them are useful.
Span<T> as a fast synchronous accessor of a continuous chunk of memory. As simple as that. When thinking about it, I tend to abstract away the source of the memory and focus on its usage. A
Span<T> can be used in synchronous code (meaning: no
async await) . Its usage usually indicates that a piece of code should be fast and performing well.
It’s worth to mention that using
foreach with it is also fast. It is based on a span’s non-allocating ref-returning implementation of
GetEnumerator. It’s almost like it was designed to be really fast ;-)
Having in mind this part of the model, that it’s a fast synchronous memory accessor, it’s good to ask a good old fashioned question: why? Why does it have to be a synchronous accessor? What’s the limitation behind it? To answer this let’s list some of the sources for a continuous chunk of memory behind the span:
The first is as simple as possible, just a slice of an array allocated on the
heap. The second is based on
Memory<T>, but for now, let’s take a leap of faith with this one. Using pointers like
void* provides a way to interact with the unmanaged world, but still does not infer that it’s synchronous. What about the last one,
stackalloc provides you the way to allocate a slice of memory on the stack of a thread that is being executed. Let’s assume that
Span<T> could be used in an asynchronous scenario. What would happen when a continuation of an async call is executed by another thread? It would result in one thread having an access to the stack of another thread. This is not how it’s supposed to be. Keeping
Span<T> a synchronous prevents this kind of behavior.
To sum it up,
Span<T> is a fast synchronous accessor of a continuous chunk of memory. It’s not the memory, it’s just a really performance friendly view of it.
Memory<T> is an actual continuous memory chunk. It can be passed in asynchronous flows. It provides a way to get the efficient synchronous accessor of it -
Span<T>. It can be based on different sources as well. Let’ consider the following examples
The first one is again the basic scenario. Take an array of
T and use a slice of it as
Memory<T>. The second is a bit more complex as it allows to build a memory in a special way. In the provided sample it uses an already pinned array (
pinning in .NET prohibits Garbage Collector from moving an object and can be useful when passing memory to the unmanaged world). Where can it be found?
Check out C# 9.0 Professional and learn the newest C# features!
Let’s take a look at
AspNetCore. It uses a custom memory pool that provides a pool for 4kb blocks of memory. All blocks are created from large slabs of memory that is already pinned. Therefore there’s no need for pinning it again whenever a memory is pinned/unpinned.
To sum it up,
Memory<T> embeds an actual memory chunk, that can be passed wherever you want and accessed using its fast synchronous accessor
Span<T>. When needed it can be pinned to obtain the pointer, but most likely, you will
Memory<T> have their readonly counterparts:
ReadOnlyMemory<T>. As the span is an synchronous accessor for the memory, the readonly span is an accessor for the readonly memory.
Sometimes a memory doesn’t come in one piece and is shattered. Still, it’d be useful to have a construct that can represents a chain, a list, a sequence of multiple pieces that represent one thing. This is the reason why
ReadOnlySequence<T> was introduced. It’s a list of
ReadOnlyMemory<T>. It’s optimized for cases where the sequence contains one element by providing properties like:
IsSingleSegment- the fast check whether it contains just one memory item
FirstSpan- the fast access to the
ReadOnlySpan<T>accessor to the first memory
The reason for this special case is following. Even when designed as a sequence, it’s much easier to deal with a single element and optimize for “one span/memory” scenario.
To sum it up,
ReadOnlySequence<T> is a linked list of
ReadOnlyMemory<T> elements. It provides a special case properties for its convenient usage where it contains a single element.
So here were are. At the end of this model. Let’s sum it up!
Span<T>- a fast synchronous accessor of a continuous chunk of memory. It’s not the memory, it’s just a really performance friendly view of it.
Memory<T>- an actual memory chunk, that can be passed wherever needed and accessed using its fast synchronous accessor
ReadOnlySpan<T>- a span but readonly
ReadOnlyMemory<T>- a memory but readonly
ReadOnlySequence<T>- a linked list of
ReadOnlyMemory<T>elements. It provides a special case properties for its convenient usage where it contains a single element.