
[DotNetBook] Span, Memory, and ReadOnlyMemory
- Tutorial
With this article, I continue to publish a series of articles, the result of which will be a book on the work of the .NET CLR, and .NET in general. For links - welcome to cat.
Memory and ReadOnlyMemory
Visual differences from the two. First, the type does not contain a restriction in the type header. That is, in other words, the type has the right to be not only on the stack, being either a local variable or a parameter of the method or its return value, but also on the heap, referring from there to some data in memory. However, this small difference creates a huge difference in behavior and capabilities compared to . Unlike , which is a means of using a certain data buffer for some methods, the type is designed to store information about the buffer, and not to work with it.Memory
Span
Memory
ref
Memory
Memory
Span
Span
Memory
Note
The chapter published on Habré is not updated and, probably, is already a little outdated. And therefore, please turn to the original for more recent text:
CLR Book: GitHub, table of contents
CLR Book: GitHub, chapter
Release 0.5.2 books, PDF: GitHub Release
From here comes the difference in the API:
Memory
It does not contain access methods to the data it manages. Instead, it has a propertySpan
and methodSlice
that return a workhorse — an instance of a typeSpan
.Memory
additionally contains a methodPin()
intended for scenarios when the stored buffer must be passed to theunsafe
code. When it is called for cases when the memory was allocated in .NET, the buffer will be pinned and will not move when the GC is triggered, returning to the user an instance of the structureMemoryHandle
that encapsulates the concept of a life spanGCHandle
that has fixed the buffer in memory:
public unsafe struct MemoryHandle : IDisposable
{
private void* _pointer;
private GCHandle _handle;
private IPinnable _pinnable;
///
/// Создает MemoryHandle для участка памяти
///
public MemoryHandle(void* pointer, GCHandle handle = default, IPinnable pinnable = default)
{
_pointer = pointer;
_handle = handle;
_pinnable = pinnable;
}
///
/// Возвращает указатель на участок памяти, который как предполагается, закреплен и данный адрес не поменяется
///
[CLSCompliant(false)]
public void* Pointer => _pointer;
///
/// Освобождает _handle и _pinnable, также сбрасывая указатель на память
///
public void Dispose()
{
if (_handle.IsAllocated)
{
_handle.Free();
}
if (_pinnable != null)
{
_pinnable.Unpin();
_pinnable = null;
}
_pointer = null;
}
}
However, to begin with, I propose to get acquainted with the whole set of classes. And as the first of them, let's take a look at the structure itself (not all type members are shown, but those that seem to be the most important):Memory
public readonly struct Memory
{
private readonly object _object;
private readonly int _index, _length;
public Memory(T[] array) { ... }
public Memory(T[] array, int start, int length) { ... }
internal Memory(MemoryManager manager, int length) { ... }
internal Memory(MemoryManager manager, int start, int length) { ... }
public int Length => _length & RemoveFlagsBitMask;
public bool IsEmpty => (_length & RemoveFlagsBitMask) == 0;
public Memory Slice(int start, int length);
public void CopyTo(Memory destination) => Span.CopyTo(destination.Span);
public bool TryCopyTo(Memory destination) => Span.TryCopyTo(destination.Span);
}
In addition to specifying the structure fields, I decided to additionally point out that there are two more internal
type constructors that work on the basis of yet another entity - MemoryManager
which will be discussed a little further and that is not something that you might have just thought about: memory manager in the classical sense. However, as well as Span
, Memory
it also contains a link to the object that will be navigated, as well as the offset and size of the internal buffer. Also, it’s worth noting that it Memory
can be created by the operator new
only on the basis of the array plus extension methods - on the basis of the string, array andArraySegment
. Those. its creation on the basis of unmanaged memory manually is not implied. However, as we see, there is some internal method for creating this structure based on MemoryManager
:
File MemoryManager.cs
public abstract class MemoryManager : IMemoryOwner, IPinnable
{
public abstract MemoryHandle Pin(int elementIndex = 0);
public abstract void Unpin();
public virtual Memory Memory => new Memory(this, GetSpan().Length);
public abstract Span GetSpan();
protected Memory CreateMemory(int length) => new Memory(this, length);
protected Memory CreateMemory(int start, int length) => new Memory(this, start, length);
void IDisposable.Dispose()
protected abstract void Dispose(bool disposing);
}
I will allow myself to argue a little with the terminology that was introduced in the CLR command, naming the type by the name MemoryManager. When I saw him, I first decided that it would be something like a memory management, but manual, other than LOH / SOH. But he was very disappointed to see reality. Perhaps you should call it by analogy with the interface: MemoryOwner.
Which encapsulates the concept of the owner of a piece of memory. In other words, if Span
- a means of working with memory, Memory
- a means of storing information about a particular site, then MemoryManager
- a means of monitoring his life, its owner. For example, we can take a type that, although written for tests, does not reflect poorly the essence of the concept of "ownership":NativeMemoryManager
internal sealed class NativeMemoryManager : MemoryManager
{
private readonly int _length;
private IntPtr _ptr;
private int _retainedCount;
private bool _disposed;
public NativeMemoryManager(int length)
{
_length = length;
_ptr = Marshal.AllocHGlobal(length);
}
public override void Pin() { ... }
public override void Unpin()
{
lock (this)
{
if (_retainedCount > 0)
{
_retainedCount--;
if (_retainedCount == 0)
{
if (_disposed)
{
Marshal.FreeHGlobal(_ptr);
_ptr = IntPtr.Zero;
}
}
}
}
}
// Другие методы
}
That is, in other words, the class provides the possibility of nested method calls Pin()
, thereby counting the resulting links from the unsafe
world.
Another entity closely related to Memory
is MemoryPool
that provides instance pooling MemoryManager
(and in fact - IMemoryOwner
):
File MemoryPool.cs
public abstract class MemoryPool : IDisposable
{
public static MemoryPool Shared => s_shared;
public abstract IMemoryOwner Rent(int minBufferSize = -1);
public void Dispose() { ... }
}
Which is designed to issue buffers of the required size for temporary use. Leased instances that implement the interface have a method that returns the leased array back to the array pool. And by default, you can use a common buffer pool, which is built on the basis of :IMemoryOwner
Dispose()
ArrayMemoryPool
File ArrayMemoryPool.cs
internal sealed partial class ArrayMemoryPool : MemoryPool
{
private const int MaximumBufferSize = int.MaxValue;
public sealed override int MaxBufferSize => MaximumBufferSize;
public sealed override IMemoryOwner Rent(int minimumBufferSize = -1)
{
if (minimumBufferSize == -1)
minimumBufferSize = 1 + (4095 / Unsafe.SizeOf());
else if (((uint)minimumBufferSize) > MaximumBufferSize)
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.minimumBufferSize);
return new ArrayMemoryPoolBuffer(minimumBufferSize);
}
protected sealed override void Dispose(bool disposing) { }
}
And on the basis of what he saw, the following picture of the world looms:
- The data type
Span
must be used in the parameters of the methods, if you mean either reading data (ReadOnlySpan
) or writing (Span
). But not the task of storing it in the class field for future use - If you need to store a link to the data buffer from the class field, you must use or - depending on the purpose
Memory
ReadOnlyMemory
MemoryManager
- is the owner of the data buffer (you can not use it: if necessary). It is necessary when, for example, there is a need to count callsPin()
. Or when you need to have knowledge on how to free memory- If
Memory
built around an unmanaged area of memory,Pin()
nothing will be done. However, this unifies the work with different types of buffers: both in the case of managed and in the case of unmanaged code, the interaction interface will be the same - Each of the types has public constructors. And this means that you can use it either
Span
directly or get a copy of it fromMemory
. SamMemory
, you can create both individually and arrange for himIMemoryOwner
a type that will hold the memory location to which you want to invokeMemory
. A special case can be any type based onMemoryManager
: some local ownership of a piece of memory (for example, with reference counting from theunsafe
world). If this requires pulling of such buffers (frequent traffic of buffers of approximately equal size is expected), you can use the typeMemoryPool
. - If it is implied that you need to work with the
unsafe
code by passing a certain data buffer there, you should use a typeMemory
: it has a methodPin
that automates fixing the buffer in the .NET heap, if one was created there. - If you have some kind of buffer traffic (for example, you solve the problem of parsing program text or some DSL), it’s worth using a type
MemoryPool
that can be organized in a very correct way, issuing buffers of a suitable size from the pool (for example, a bit larger if no suitable one is found, but with croppingoriginalMemory.Slice(requiredSize)
so as not to fragment the pool)
Link to the whole book
CLR Book: GitHub
Release 0.5.0 books, PDF: GitHub Release