Этой статьей я продолжаю публиковать целую серию статей, результатом которой будет книга по работе .NET CLR, и .NET в целом. За ссылками — добро пожаловать под кат.
Memory<T> и ReadOnlyMemory<T>
Визуальных отличий Memory<T> от Span<T> два. Первое — тип Memory<T> не содержит ограничения ref в заголовке типа. Т.е., другими словами, тип Memory<T> имеет право находиться не только на стеке, являясь либо локальной переменной либо параметром метода либо его возвращаемым значением, но и находиться в куче, ссылаясь оттуда на некоторые данные в памяти. Однако эта маленькая разница создает огромную разницу в поведении и возможностях Memory<T> в сравнении с Span<T>. В отличии от Span<T>, который представляет собой средство пользования неким буфером данных для некоторых методов, тип Memory<T> предназначен для хранения информации о буфере, а не для работы с ним.
Примечание
Глава, опубликованная на Хабре не обновляется и возможно, уже несколько устарела. А потому, прошу обратиться за более свежим текстом к оригиналу:
CLR Book: GitHub, оглавление
CLR Book: GitHub, глава
Релиз 0.5.2 книги, PDF: GitHub Release
Отсюда возникает разница в API:
Memory<T>не содержит методов доступа к данным, которыми он заведует. Вместо этого он имеет свойствоSpanи методSlice, которые возвращают рабочую лошадку — экземпляр типаSpan.Memory<T>дополнительн�� содержит методPin(), предназначенный для сценариев, когда хранящийся буфер необходимо передать вunsafeкод. При его вызове для случаев, когда память была выделена в .NET, буфер будет закреплен (pinned) и не будет перемещаться при срабатывании GC, возвращая пользователю экземпляр структурыMemoryHandle, инкапсулирующей в себе понятие отрезка жизниGCHandle, закрепившего буфер в памяти:
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;
}
}Однако, для начала предлагаю познакомиться со всем набором классов. И в качестве первого из них, взглянем на саму структуру Memory<T> (показаны не все члены типа, а показавшиеся наиболее важными):
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);
}Помимо указания полей структуры я решил дополнительно указать на то, что существует еще два internal конструктора типа, работающих на основании еще одной сущности — MemoryManager, речь о котором зайдет несколько дальше и что не является чем-то, о чем вы, возможно, только что подумали: менеджером памяти в классическом понимании. Однако, как и Span, Memory точно также содержит в себе ссылку на объект, по которому будет производить навигация, а также смещение и размер внутреннего буфера. Также, дополнительно, стоит отметить что Memory может быть создан оператором new только на основании массива плюс методами расширения — на основании строки, массива и ArraySegment. Т.е. его создание на основании unmanaged памяти вручную не подразумевается. Однако, как мы видим, существует некий внутренний метод создания этой структуры на основании MemoryManager:
Файл MemoryManager.cs
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);
}Я позволю себе несколько поспорить с терминологией, которую ввели в команде CLR, назвав тип именем MemoryManager. Когда я его увидел, то сначала решил что это будет что-то типа менеджмента памяти, но ручного, отличного от LOH/SOH. Но был сильно разочарован, увидев реальность. Возможно, стоило назвать его по анаолгии с интерфейсом: MemoryOwner.
Которая инкапсулирует в себе понятие владельца участка памяти. Другими словами если Span — средство работы с памятью, Memory — средство хранения информации о конкретном участке, то MemoryManager — средство контроля его жизни, его владелец. Для примера можно взять тип NativeMemoryManager<T>, который хоть и написан для тестов, однако не плохо отражает суть понятия "владение":
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;
}
}
}
}
}
// Другие методы
}Т.е., другими словами, класс обеспечивает возможность вложенных вызовов метода Pin() подсчитывая тем самым образующиеся ссылки из unsafe мира.
Еще одной сущностью, тесно связанной с Memory является MemoryPool, который о��еспечивает пулинг экземпляров MemoryManager (а по факту — IMemoryOwner):
Файл MemoryPool.cs
public abstract class MemoryPool<T> : IDisposable
{
public static MemoryPool<T> Shared => s_shared;
public abstract IMemoryOwner<T> Rent(int minBufferSize = -1);
public void Dispose() { ... }
}Который предназначен для выдачи буферов необходимого размера во временное пользование. Арендуемые экземпляры, реализующие интерфейс IMemoryOwner<T> имеют метод Dispose(), который возвращает арендованный массив обратно в пул массивов. Причем по умолчанию вы можете пользоваться общим пулом буферов, который построен на основе ArrayMemoryPool:
Файл ArrayMemoryPool.cs
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) { }
}И на основании увиденного, вырисовывается следующая картина мира:
- Тип данных
Spanнеобходимо использовать в параметрах методов, если вы подразумеваете либо считывание данных (ReadOnlySpan), либо запись (Span). Но не задачу его сохранения в поле класса для использования в будущем - Если вам необходимо хранить ссылку на буфер данных из поля класса, необходимо использовать
Memory<T>илиReadOnlyMemory<T>— в зависимости от целей MemoryManager<T>— это владелец буфера данных (можно не использовать: по необходимости). Необходим, когда, например, встает необходимость подсчитывать вызовыPin(). Или когда необходимо обладать знаниями о том, как освобождать память- Если
Memoryпостроен вокруг неуправляемого участка памяти,Pin()ничего не сделает. Однако, это унифицирует работу с разными типами буферов: как в случае управляемого так и в случае неуправляемого кода интерфейс взаимодействия будет одинаковым - Каждый из типов имеет публичные конструкторы. А это значит, что вы можете пользоваться как
Spanнапрямую, так и получать его экземпляр изMemory. СамMemoryвы можете создать как отдельно, так и организовать для негоIMemoryOwnerтип, который будет владеть участком памяти, на который будет ссылатьсяMemory. Частным случаем может являться любой тип, основанный наMemoryManager: некоторое локальное владение участком памяти (например, с подсчетом ссылок изunsafeмира). Если при этом необходим пуллинг таких буферов (ожидается частый траффик буферов примерно равного размера), можно возпользоваться типомMemoryPool. - Если подразумевается что вам необходимо работать с
unsafeкодом, передавая туда некий буфер данных, стоит использовать типMemory: он имеет методPin, автоматизирующий фиксацию буфера в куче .NET, если тот был там создан. - Если же вы имеете некий трафик буферов (например, вы решаете задачу парсинга текста программы или какого-то DSL), стоит воспользоваться типом
MemoryPool, который можно организовать очень правильным образом, выдавая из пула буферы подходящего размера (например, немного большего если не нашлось подходящего, но с обрезкойoriginalMemory.Slice(requiredSize)чтобы не фрагментировать пул)
Ссылка на всю книгу
CLR Book: GitHub
Релиз 0.5.0 книги, PDF: GitHub Release
CLR Book:
Релиз 0.5.2 книги, PDF: 
