[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.MemorySpanMemoryrefMemoryMemorySpanSpanMemory


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:



From here comes the difference in the API:


  • MemoryIt does not contain access methods to the data it manages. Instead, it has a property Spanand method Slicethat return a workhorse — an instance of a type Span.
  • Memoryadditionally contains a method Pin()intended for scenarios when the stored buffer must be passed to the unsafecode. 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 structure MemoryHandlethat encapsulates the concept of a life span GCHandlethat 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 internaltype constructors that work on the basis of yet another entity - MemoryManagerwhich 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, Memoryit 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 Memorycan be created by the operator newonly 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


NativeMemoryManager.cs File


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 unsafeworld.


Another entity closely related to Memoryis MemoryPoolthat 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 :IMemoryOwnerDispose()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 Spanmust 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 purposeMemoryReadOnlyMemory
  • 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 calls Pin(). Or when you need to have knowledge on how to free memory
  • If Memorybuilt 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 Spandirectly or get a copy of it from Memory. Sam Memory, you can create both individually and arrange for him IMemoryOwnera type that will hold the memory location to which you want to invoke Memory. A special case can be any type based on MemoryManager: some local ownership of a piece of memory (for example, with reference counting from the unsafeworld). If this requires pulling of such buffers (frequent traffic of buffers of approximately equal size is expected), you can use the type MemoryPool.
  • If it is implied that you need to work with the unsafecode by passing a certain data buffer there, you should use a type Memory: it has a method Pinthat 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 MemoryPoolthat 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 cropping originalMemory.Slice(requiredSize)so as not to fragment the pool)

Link to the whole book




Also popular now: