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()
: создает пустой объект SpanSpan(T item)
: создает объект Span с одним элементом itemSpan(T[] array)
: создает объект Span из массива arraySpan(void* pointer, int length)
: создает объект Span, который получает length байт памяти, начиная с указателя pointerSpan(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 значением valueT[] ToArray()
: преобразует Span в массивSpan<T> Slice(int start, int length)
: выделяет из Span length элементов начиная с индекса start в виде другого Spanvoid Clear()
: очищает Spanvoid CopyTo(Span<T> destination)
: копирует элементы текущего Span в другой Spanbool 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 */);
}
}
//....
}
Иными словами мы можем выделить три основных достоинства:
- Производительность:
Span<T>
иMemory<T>
устраняет необходимость в копировании данных, сокращает объем выделяемой памяти и повышает производительность. - Безопасность:
Они обеспечивают безопасность памяти, предотвращая переполнение буфера и обеспечивая безопасность типов. - Гибкость:
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>
, вы сможете создавать более эффективные и производительные приложения.