📜 ⬆️ ⬇️

[DotNetBook] Span, Memory and ReadOnlyMemory

With this article, I continue to publish a whole series of articles, the result of which will be a book on the work of the .NET CLR, and .NET as a whole. For links - welcome under cat.


Memory <T> and ReadOnlyMemory <T>


There are two visual differences between Memory<T> and Span<T> . First, the Memory<T> does not contain a ref constraint in the type header. That is, in other words, the Memory<T> 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 the memory. However, this small difference creates a huge difference in the behavior and capabilities of Memory<T> in comparison with Span<T> . Unlike Span<T> , which is a means of using a kind of data buffer for some methods, the Memory<T> used to store information about the buffer, and not to work with it.


This article is the second of a cycle about Span <T> and Memory <T>. It is an introductory one for Memory <T> in the sense that I decided to paint a common terminology here, but I decided to put together examples of sharing in a separate article


Note


The chapter published on Habré is not updated and it is possible that it is already somewhat outdated. So, please ask for a more recent text to the original:



Hence the difference in the API:



 public unsafe struct MemoryHandle : IDisposable { private void* _pointer; private GCHandle _handle; private IPinnable _pinnable; /// <summary> ///  MemoryHandle    /// </summary> public MemoryHandle(void* pointer, GCHandle handle = default, IPinnable pinnable = default) { _pointer = pointer; _handle = handle; _pinnable = pinnable; } /// <summary> ///     ,   ,       /// </summary> [CLSCompliant(false)] public void* Pointer => _pointer; /// <summary> ///  _handle  _pinnable,      /// </summary> public void Dispose() { if (_handle.IsAllocated) { _handle.Free(); } if (_pinnable != null) { _pinnable.Unpin(); _pinnable = null; } _pointer = null; } } 

However, for a start I suggest to get acquainted with the whole set of classes. And as the first of them, let's take a look at the Memory<T> structure itself (not all members of the type are shown, but the ones that seem most important):


  public readonly struct Memory<T> { private readonly object _object; private readonly int _index, _length; public Memory(T[] array) { ... } public Memory(T[] array, int start, int length) { ... } internal Memory(MemoryManager<T> manager, int length) { ... } internal Memory(MemoryManager<T> manager, int start, int length) { ... } public int Length => _length & RemoveFlagsBitMask; public bool IsEmpty => (_length & RemoveFlagsBitMask) == 0; public Memory<T> Slice(int start, int length); public void CopyTo(Memory<T> destination) => Span.CopyTo(destination.Span); public bool TryCopyTo(Memory<T> destination) => Span.TryCopyTo(destination.Span); } 

In addition to specifying the structure fields, I decided to additionally indicate that there are two more internal type constructors that work on the basis of another entity, the MemoryManager , which will be discussed a little further and which is not something that you may have just thought: memory manager in the classic sense. However, like Span , Memory also contains a reference to the object that will be used for navigation, as well as the offset and size of the internal buffer. Also, in addition, it is worth noting that Memory can be created by the new operator only on the basis of an array plus extension methods — on the basis of a string, an array, and an ArraySegment . Those. its creation based on unmanaged memory is not implied. However, as we can see, there is some internal method for creating this structure based on a MemoryManager :


MemoryManager.cs file


 public abstract class MemoryManager<T> : IMemoryOwner<T>, IPinnable { public abstract MemoryHandle Pin(int elementIndex = 0); public abstract void Unpin(); public virtual Memory<T> Memory => new Memory<T>(this, GetSpan().Length); public abstract Span<T> GetSpan(); protected Memory<T> CreateMemory(int length) => new Memory<T>(this, length); protected Memory<T> CreateMemory(int start, int length) => new Memory<T>(this, start, length); void IDisposable.Dispose() protected abstract void Dispose(bool disposing); } 

I will allow myself to somewhat argue with the terminology that was introduced in the CLR command, calling the type the name MemoryManager. When I saw it, I first decided that it would be something like a memory management, but manual, different from LOH / SOH. But he was very disappointed to see the reality. Perhaps, it was worthwhile to call it by anaolgy with the interface: MemoryOwner.

Which encapsulates in itself the concept of the owner of a section of memory. In other words, if Span is a means of working with memory, Memory is a means of storing information about a specific area, then MemoryManager is a means of controlling its life, its owner. For example, you can take the type NativeMemoryManager<T> , which, although written for tests, does not badly reflect the essence of the concept of "ownership":


NativeMemoryManager.cs file


 internal sealed class NativeMemoryManager : MemoryManager<byte> { 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 calls to the Pin() method, thereby counting the resulting links from the unsafe world.


Another entity closely associated with Memory is MemoryPool , which provides for the pooling of MemoryManager instances (and in fact, IMemoryOwner ):


MemoryPool.cs file


 public abstract class MemoryPool<T> : IDisposable { public static MemoryPool<T> Shared => s_shared; public abstract IMemoryOwner<T> Rent(int minBufferSize = -1); public void Dispose() { ... } } 

Which is intended for issuing buffers of the required size for temporary use. Rented instances that implement the IMemoryOwner<T> interface have a Dispose() method that returns the rented array back to the pool of arrays. And by default you can use a common buffer pool, which is built on the basis of ArrayMemoryPool :


ArrayMemoryPool.cs file


 internal sealed partial class ArrayMemoryPool<T> : MemoryPool<T> { private const int MaximumBufferSize = int.MaxValue; public sealed override int MaxBufferSize => MaximumBufferSize; public sealed override IMemoryOwner<T> Rent(int minimumBufferSize = -1) { if (minimumBufferSize == -1) minimumBufferSize = 1 + (4095 / Unsafe.SizeOf<T>()); 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 emerges:



Link to the whole book




')

Source: https://habr.com/ru/post/420051/


All Articles