Этой статьей я продолжаю публиковать целую серию статей, результатом которой будет книга по работе .NET CLR, и .NET в целом. За ссылками — добро пожаловать по кат.
Memory<T> и ReadOnlyMemory<T>
Визуальных отличий Memory<T> от Span<T> два. Первое — тип Memory<T> не содержит ограничения ref в заголовке типа. Т.е., другими словами, тип Memory<T> имеет право находиться не только на стеке, являясь либо локальной переменной либо параметром метода либо его возвращаемым значением, но и находиться в куче, ссылаясь оттуда на некоторые данные в памяти. Однако эта маленькая разница создает огромную разницу в поведении и возможностях Memory<T> в сравнении с Span<T>. В отличии от Span<T>, который представляет собой средство пользования неким буфером данных для некоторых методов, тип Memory<T> предназначен для хранения информации о буфере, а не для работы с ним.
Эта статья — вторая из цикла про Span<T> и Memory<T>. Она является вводной для Memory<T> в том плане что здесь я решил расписать общую терминилогию, а вот примеры совместного использования — решил вывести в отдельную статью
- Span<T>: новый тип данных .NET
- Span<T> Memory<T> и ReadOnlyMemory<T> (эта статья)
- Практика использования Span<T> и Memory<T>
Отсюда возникает разница в 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
ссылка на оригинал статьи https://habr.com/post/420051/
CLR Book:
Релиз 0.5.0 книги, PDF:
Добавить комментарий