Хабрахабр

[DotNetBook]: Span, Memory и ReadOnlyMemory

NET CLR, и . Этой статьей я продолжаю публиковать целую серию статей, результатом которой будет книга по работе . За ссылками — добро пожаловать по кат. NET в целом.

Memory<T> и ReadOnlyMemory<T>

Первое — тип Memory<T> не содержит ограничения ref в заголовке типа. Визуальных отличий Memory<T> от Span<T> два. Однако эта маленькая разница создает огромную разницу в поведении и возможностях Memory<T> в сравнении с Span<T>. Т.е., другими словами, тип Memory<T> имеет право находиться не только на стеке, являясь либо локальной переменной либо параметром метода либо его возвращаемым значением, но и находиться в куче, ссылаясь оттуда на некоторые данные в памяти. В отличии от Span<T>, который представляет собой средство пользования неким буфером данных для некоторых методов, тип Memory<T> предназначен для хранения информации о буфере, а не для работы с ним.

Она является вводной для Memory<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
/// <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); }

Однако, как и Span, Memory точно также содержит в себе ссылку на объект, по которому будет производить навигация, а также смещение и размер внутреннего буфера. Помимо указания полей структуры я решил дополнительно указать на то, что существует еще два internal конструктора типа, работающих на основании еще одной сущности — MemoryManager, речь о котором зайдет несколько дальше и что не является чем-то, о чем вы, возможно, только что подумали: менеджером памяти в классическом понимании. Т.е. Также, дополнительно, стоит отметить что Memory может быть создан оператором new только на основании массива плюс методами расширения — на основании строки, массива и ArraySegment. Однако, как мы видим, существует некий внутренний метод создания этой структуры на основании MemoryManager: его создание на основании unmanaged памяти вручную не подразумевается.

Файл 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);
}

Когда я его увидел, то сначала решил что это будет что-то типа менеджмента памяти, но ручного, отличного от LOH/SOH. Я позволю себе несколько поспорить с терминологией, которую ввели в команде CLR, назвав тип именем MemoryManager. Возможно, стоило назвать его по анаолгии с интерфейсом: MemoryOwner.
Но был сильно разочарован, увидев реальность.

Другими словами если Span — средство работы с памятью, Memory — средство хранения информации о конкретном участке, то MemoryManager — средство контроля его жизни, его владелец. Которая инкапсулирует в себе понятие владельца участка памяти. Для примера можно взять тип NativeMemoryManager<T>, который хоть и написан для тестов, однако не плохо отражает суть понятия "владение":

Файл NativeMemoryManager.cs

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) чтобы не фрагментировать пул)

Ссылка на всю книгу

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть