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.
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
- Span <T>: a new .NET data type
- Span <T> Memory <T> and ReadOnlyMemory <T> (this article)
- Practice using Span <T> and Memory <T>
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:
CLR Book: GitHub, table of contents
CLR Book: GitHub, chapter
Release 0.5.2 of the book, PDF: GitHub Release
Hence the difference in the API:
Memory<T>
does not contain data access methods that it manages. Instead, it has the Span
property and the Slice
method, which return the workhorse — an instance of the Span
type.Memory<T>
additionally contains the Pin()
method, intended for scenarios where the stored buffer needs to be passed in unsafe
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 MemoryHandle
structure encapsulating the concept of the GCHandle
life GCHandle
that fixed the buffer in memory: 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":
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:
Span
data type must be used in method parameters if you mean either reading data ( ReadOnlySpan
) or writing ( Span
). But not the task of keeping it in the class field for future use.Memory<T>
or ReadOnlyMemory<T>
- depending on the purpose.MemoryManager<T>
is the owner of the data buffer (you can not use it: if necessary). Necessary when, for example, it becomes necessary to count the calls of Pin()
. Or when you need to have knowledge of how to free memoryMemory
built around an unmanaged memory, Pin()
do nothing. However, this unifies working with different types of buffers: both in the case of managed and unmanaged code, the interaction interface will be the same.Span
directly or receive an instance of it from Memory
. You can create a Memory
yourself either separately or organize an IMemoryOwner
type for it that will own the memory location that Memory
will refer to. A special case can be any type based on MemoryManager
: some local ownership of a section of memory (for example, with reference counting from the unsafe
world). If you need to pull such buffers (frequent traffic of buffers of approximately equal size is expected), you can use the MemoryPool
type.unsafe
code, transferring a certain data buffer there, you should use the Memory
type: it has a Pin
method that automates fixing the buffer in the .NET heap if it was created there.MemoryPool
type, which can be organized in a very correct way, issuing buffers of suitable size from the pool (for example, a little more if you don’t find a suitable , but with the originalMemory.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
Source: https://habr.com/ru/post/420051/
All Articles