Класс MemoryOwner<T>

Класс MemoryOwner<T> — это тип буфера IMemoryOwner<T>, реализующий . Он включает в себя внедренное свойство длины и ряд api-интерфейсов, ориентированных на производительность. Фактически это облегченная оболочка для типа ArrayPool<T>, с некоторыми дополнительными вспомогательными утилитами.

API платформы:MemoryOwner<T>, AllocationMode

Принцип работы

MemoryOwner<T> имеет следующие основные функции:

  • Одна из основных проблем с массивами, возвращаемыми ArrayPool<T> API и IMemoryOwner<T> экземплярами, возвращаемыми MemoryPool<T> API, заключается в том, что указанный размер используется только в качестве минимального размера: фактический размер возвращаемых буферов может быть больше. MemoryOwner<T> решает эту проблему, также сохраняя исходный запрошенный размер, поэтому экземпляры Memory<T> и Span<T>, полученные из него, никогда не должны быть срезаны вручную.
  • При использовании IMemoryOwner<T> для получения базового буфера Span<T> необходимо сначала получить экземпляр Memory<T>, а затем Span<T>. Этот процесс довольно дорогой и часто ненужный, так как может не потребоваться промежуточный Memory<T>. Вместо этого, MemoryOwner<T> имеет дополнительное свойство, которое чрезвычайно легковесно, потому что оно напрямую оборачивает внутренний массив Span, арендованный из пула.
  • Пул по умолчанию не очищает буферы, которые он арендует. Если пул не очищал буферы перед их возвращением, они могут содержать мусорные данные. Как правило, необходимо очистить эти арендованные буферы вручную, что может быть подробным, особенно при частом выполнении. MemoryOwner<T> предлагает более гибкий подход через Allocate(int, AllocationMode) API. Этот метод не только выделяет новый экземпляр точно запрошенного размера, но и позволяет указать, какой режим выделения следует использовать: тот же, что и у ArrayPool<T>, или другой, который автоматически очищает арендованный буфер.
  • Иногда вы можете арендовать буфер с большим размером, чем вам нужно, а затем изменить его размер. Как правило, вам потребуется арендовать новый буфер и скопировать интересующий регион из старого буфера. Вместо этого MemoryOwner<T> предоставляет Slice(int, int) API, который возвращает новый экземпляр, завернув указанную область интереса. Этот подход избегает аренды нового буфера и необходимости полного копирования элементов.

Синтаксис

Ниже приведен пример аренды буфера и получения экземпляра Memory<T> :

// Be sure to include this using at the top of the file:
using Microsoft.Toolkit.HighPerformance.Buffers;

using (MemoryOwner<int> buffer = MemoryOwner<int>.Allocate(42))
{
    // Both memory and span have exactly 42 items.
    Memory<int> memory = buffer.Memory;
    Span<int> span = buffer.Span;

    // Writing to the span modifies the underlying buffer.
    span[0] = 42;
}

В этом примере вы используете блок using, чтобы объявить буфер MemoryOwner<T>. Этот подход особенно полезен, так как базовый массив автоматически возвращается в пул в конце блока. Если у вас нет непосредственного управления сроком службы экземпляра MemoryOwner<T>, буфер возвращается в пул, когда сборщик мусора завершает обработку объекта. В обоих случаях арендованные буферы всегда правильно возвращаются в общий пул.

Когда следует использовать этот тип?

Можно использовать MemoryOwner<T> в качестве типа буфера общего назначения. Он сводит к минимуму количество выделений памяти с течением времени, так как повторно использует одни и те же массивы из общего пула. Распространенный случай использования заключается в замене new T[] выделения массивов, особенно при выполнении повторяющихся операций, которые либо требуют временного буфера для работы, либо создают буфер в результате.

Предположим, что у вас есть набор данных, состоящий из ряда двоичных файлов, и необходимо считывать все эти файлы и обрабатывать их каким-то образом. Чтобы правильно разделить код, можно написать метод, который просто считывает один двоичный файл, который может выглядеть следующим образом:

public static byte[] GetBytesFromFile(string path)
{
    using Stream stream = File.OpenRead(path);

    byte[] buffer = new byte[(int)stream.Length];

    stream.Read(buffer, 0, buffer.Length);

    return buffer;
}

Обратите внимание на new byte[] выражение. Если вы читаете большое количество файлов, вы выделяете множество новых массивов, что оказывает большое давление на сборщик мусора. Может потребоваться рефакторинг этого кода с помощью буферов, арендованных из пула, например:

public static (byte[] Buffer, int Length) GetBytesFromFile(string path)
{
    using Stream stream = File.OpenRead(path);

    byte[] buffer = ArrayPool<T>.Shared.Rent((int)stream.Length);

    stream.Read(buffer, 0, (int)stream.Length);

    return (buffer, (int)stream.Length);
}

Используя этот подход, вы арендуете буферы из пула, что означает, что в большинстве случаев вы избегаете выделения. Кроме того, поскольку арендованные буферы по умолчанию не очищаются, вы также можете сэкономить время на их заполнении нулями, что дает небольшое улучшение производительности. В предыдущем примере загрузка 1000 файлов уменьшает общий размер выделения с примерно 1 МБ до всего лишь 1 024 байт. Вы эффективно выделяете и повторно используете только один буфер.

Предыдущий код имеет две основные проблемы:

  • ArrayPool<T> может возвращать буферы с размером, превышающим запрошенный размер. Чтобы обойти эту проблему, необходимо вернуть кортеж, который также указывает фактический используемый размер в арендованном буфере.
  • Просто возвращая массив, необходимо проявлять особую осторожность, чтобы корректно отслеживать его жизненный цикл и возвращать его в соответствующий пул. Вы можете обойти эту проблему, используя MemoryPool<T> вместо этого и возвращая IMemoryOwner<T> экземпляр, но у вас по-прежнему проблема арендованных буферов с большим размером, чем вам нужно. Кроме того, IMemoryOwner<T> имеет некоторые накладные расходы при получении Span<T> для обработки, так как это интерфейс, и также из-за того, что вам всегда нужно получить Memory<T> экземпляр, а затем Span<T>.

Чтобы решить обе эти проблемы, можно повторно выполнить рефакторинг этого кода с помощью MemoryOwner<T>:

public static MemoryOwner<byte> GetBytesFromFile(string path)
{
    using Stream stream = File.OpenRead(path);

    MemoryOwner<byte> buffer = MemoryOwner<byte>.Allocate((int)stream.Length);

    stream.Read(buffer.Span);

    return buffer;
}

Возвращаемый IMemoryOwner<byte> экземпляр удаляет базовый буфер и возвращает его в пул при вызове метода IDisposable.Dispose . Вы можете использовать его, чтобы получить экземпляр Memory<T> или Span<T>, для взаимодействия с загруженными данными, а затем удалить экземпляр, когда он больше не нужен. Кроме того, все MemoryOwner<T> свойства (например MemoryOwner<T>.Span) учитывают исходный запрошенный размер, поэтому вам больше не нужно вручную отслеживать действующий размер в арендованном буфере.

Примеры

Дополнительные примеры можно найти в модульных тестах.