Рубрики
Программирование

Span и Memory в .NET

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

Что же такое Span и Memory? Начнём с определений.

Span

Span<T> это стек-онли тип который предоставляет типобезопасный и эффективный способ доступа к непрерывным областям произвольной памяти. В отличии от массивов, Span<T> может указывать на любую область памяти, включая массивы, объекты расположенные на стеке и неуправляемую память. Эта гибкость делает Span<T> невероятно полезным для приложений в которых важна производительность. Он также реализуют слайсинг (нарезку) и позволяют работать с разделом массива, строки или блока памяти без дублирования исходного фрагмента памяти.

Memory

Memory<T> во многом похож на Span<T>, но может располагаться в куче. Это делает Memory<T> пригодным для сценариев где данные должны располагаться вне текущего стека, таких как использование асинхронных операций. Тип Memory<T> при необходимости может быть конвертирован в Span<T>, обеспечивая гибкость и безопасность.

Методы Span

Создание Span

Для создания объекта Span можно применять один из его конструкторов:

  • Span(): создает пустой объект Span
  • Span(T item): создает объект Span с одним элементом item
  • Span(T[] array): создает объект Span из массива array
  • Span(void* pointer, int length): создает объект Span, который получает length байт памяти, начиная с указателя pointer
  • Span(T[] array, int start, int length): создает объект Span, который получает из массива array length элементов, начиная с индекса start

Давайте посмотрим на примере. Вот так выглядит простейшее создание Span:

Span<string> numbers = ["One", "Two", "Three"]

Также можно создать Span на основе каких-то других наборов данных:

string[] numbers =  { "One", "Two", "Three" };
Span<string> numbersSpan = new Span<string>(people);

Кроме того, мы можем непосредственно присвоить массив, и он неявно будет преобразован в Span:

string[] numbers =  { "One", "Two", "Three" };
Span<string> numbersSpan = people;

Основные методы Span:

  • void Fill(T value): заполняет все элементы Span значением value
  • T[] ToArray(): преобразует Span в массив
  • Span<T> Slice(int start, int length): выделяет из Span length элементов начиная с индекса start в виде другого Span
  • void Clear(): очищает Span
  • void CopyTo(Span<T> destination): копирует элементы текущего Span в другой Span
  • bool TryCopyTo(Span<T> destination): копирует элементы текущего Span в другой Span, но при этом также возвращает значение bool, которое указывает, удачно ли прошла операция копирования

В целом работа со Span очень похожа на работу с массивами. Мы точно так же можем получать, устанавливать или перебирать данные также, как в массиве:

string[] numbers = { "One", "Two", "Three" };
Span<string> numbersSpan = numbers;
numbersSpan[1] = "Zwei";                 // переустановка значения элемента
Console.WriteLine(numbersSpan[2]);       // получение элемента
Console.WriteLine(numbersSpan.Length);   // получение длины Span
 
// перебор Span
foreach (var s in numbersSpan)
{
    Console.WriteLine(s);
}

Хорошо, со Span разобрались. Но Memory-то нам зачем? Как и Span<T>, Memory<T> представляет непрерывную область памяти. Однако, в отличие от Span<T>, Memory<T> не является структурой ref. Это означает, что Memory<T> может быть помещен в управляемую кучу, в то время как Span<T> — нет, даже при помощи упаковки. В результате структура Memory<T> не имеет тех же ограничений, что и экземпляр Span<T>. В частности его можно использовать:

  • как поле в классе;
  • как с await, так и с yield;
  • в качестве параметров дженериков.

Кроме того, важно помнить что Memory может начинаться не с нулевого индекса.

Преимущества от использования Span и Memory

Но зачем нам Span если есть обычные массивы? Здесь тоже лучше обратиться к примеру:

int[] data = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
11, 12, 13, 14, 15, 16, 17, 18, 19, 20 };

// Работа с обычным массивом
int[] firstPart = new int[10];     // выделяем память для первой половины
int[] secondPart = new int[10];    // выделяем память для второй половины
Array.Copy(data, 0, firstPart, 0, 10);
Array.Copy(data, 10, secondPart, 0, 10);

// Работа со Span
Span<int> dataSpan = temperatures;
 
Span<int> firstPartSpan = dataSpan.Slice(0, 10);      // нет выделения памяти под данные
Span<int> secondPartSpan = dataSpan.Slice(10, 10);    // нет выделения памяти под данные

Здесь для хранения данных нам требуется целых два дополнительных массива данных, которые мало того что требуют больше места в памяти, так ещё и нужно сначала скопировать данные, чтобы заполнить ими массивы. В случае же со Span нам не требуется ни копировать данные, ни выделять под них память. По сути мы работаем с указателем в рамках стека и чтобы убедиться в этом мы можем посмотреть на исходный код типа:

public readonly ref struct Span<T>
{
    //....
    public ref T this[int index]
        {
            [Intrinsic]
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            [NonVersionable]
            get
            {
                if ((uint)index >= (uint)_length)
                    ThrowHelper.ThrowIndexOutOfRangeException();
                return ref Unsafe.Add(ref _reference, (nint)(uint)index /* force zero-extension */);
            }
        }
    //....
}

Иными словами мы можем выделить три основных достоинства:

  1. Производительность:
    Span<T> и Memory<T> устраняет необходимость в копировании данных, сокращает объем выделяемой памяти и повышает производительность.
  2. Безопасность:
    Они обеспечивают безопасность памяти, предотвращая переполнение буфера и обеспечивая безопасность типов.
  3. Гибкость:
    Span<T> работает с различными типами, в то время как Memory<T> расширяет эти возможности для долгоживущих объектов.

Распространенные варианты использования

1. Срезы массивов

Используя Span<T> можно эффективно создавать срезы (slices) массивов без копирования данных.

int[] array = { 1, 2, 3, 4, 5 };
Span<int> slice = new Span<int>(array, 1, 3); // Срез массива с индекса 1 до 3

foreach (var item in slice)
{
    Console.WriteLine(item); // Вывод: 2, 3, 4
}

2. Манипуляции со строками

Span<T> может использоваться для эффективной манипуляции строками без выделения памяти.

string text = "Hello, World!";
ReadOnlySpan<char> span = text.AsSpan(7, 5); // "World"

Console.WriteLine(span.ToString()); // Вывод: "World"

В данном примере применяется ReadOnlySpan<T> структура предназначенная для неизменяемых данных. Здесь с помощью метода AsSpan() мы преобразуем строку в объект ReadOnlySpan<char> и затем выделяем из него диапазон символов «world». Поскольку ReadOnlySpan предназначен только для чтения, то соответственно мы не можем изменить через него данные, но получить можем. В остальном работа с ReadOnlySpan идет так же, как с Span.

3. Парсинг бинарных данных

При работе с бинарными данным, Span<T> позволяет парсить и обрабатывать данные буфера без выделения памяти.

byte[] buffer = { 0x01, 0x02, 0x03, 0x04, 0x05 };
Span<byte> span = new Span<byte>(buffer);

// Парсим 4 байта начиная с индекса 1
int value = BitConverter.ToInt32(span.Slice(1, 4));
Console.WriteLine(value); // Вывод: 67305985

4. Асинхронные операции с Memory

Memory<T> идеально подходит для асинхронных операций где данные необходимо размещать вне текущего стека.

async Task ProcessDataAsync(Memory<byte> memory)
{
    // Симулируем асинхронную операцию
    await Task.Delay(1000);

    // Обрабатываем данные
    Span<byte> span = memory.Span;
    foreach (var b in span)
    {
        Console.WriteLine(b);
    }
}

byte[] buffer = { 0x01, 0x02, 0x03, 0x04, 0x05 };
Memory<byte> memory = new Memory<byte>(buffer);

await ProcessDataAsync(memory); // Вывод: 1, 2, 3, 4, 5

Когда не стоит использовать Span и Memory

Span

  • Долгоживущие объектыSpan<T> тип который может быть расположен только на стеке, поэтому его не следует использовать для долгоживущих объектов или хранить в объектах, выделенных в куче. Вместо него для этих целей следует Memory<T> в массивы.
  • Асинхронные операции: избегайте использования Span<T> в асинхронных методах, так как данный тип не применим для случаев когда данные должны быть расположены вне текущего стека.

Memory

  • Сложное устройство памяти: Если логика вашего приложения требует сложного управления состоянием или включает в себя несколько уровней абстракции, использование Memory<T> может усложнить понимание кода. В таких случаях лучше использовать традиционный подход.

Заключение

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

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

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