Эта статья познакомит вас с новыми типами, представленными в C# 7.2: Span и Memory. Я подробно разберу Span<T> и Memory<T> и покажу, как работать с ними в C#.

Что вам понадобится

Если вы хотите сами протестировать примеры кода, приведенные в этой статье, на вашем компьютере должно быть установлено следующее:

  • Visual Studio 2022

  • .NET 6.0

  • ASP.NET 6.0 Runtime

Если на вашем компьютере еще не установлена ​​Visual Studio 2022, вы можете скачать ее отсюда.

Типы памяти, поддерживаемые в .NET

Microsoft .NET позволяет работать с тремя типами памяти:

  • Стековая память: находится в стеке (Stack) и выделяется с помощью ключевого слова stackalloc 

  • Управляемая память: находится в куче (heap) и управляется сборщиком мусора (GC)

  • Неуправляемая память: находится в неуправляемой куче (unmanaged heap) и выделяется путем вызова методов Marshal.AllocHGlobal или Marshal.AllocCoTaskMem.

Типы, добавленные в .NET Core 2.1

Новые типы, представленные в .NET Core 2.1:

  • System.Span: является типобезопасным memory-safe представлением непрерывной области произвольной памяти.

  • System.ReadOnlySpan: является типобезопасным memory-safe представлением непрерывной области произвольной памяти предназначенной только для чтения.

  • System.Memory: представляет непрерывную область памяти.

  • System.ReadOnlyMemory: Подобно ReadOnlySpan, этот тип представляет собой непрерывную область памяти, доступную только для чтения. Однако, в отличие от ReadOnlySpan, он не является ByRef типом.

Доступ к непрерывной памяти: Span и Memory

Нам все чаще приходится работать с огромными объемами данных в наших приложениях. То, как мы обрабатываем строки, имеет решающее значение для производительности любого приложения, поскольку существуют практические приемы, реализовав которые, мы можем избежать излишних аллокаций памяти. Мы можем использовать небезопасные (unsafe) блоки кода и указатели для прямой манипуляции памятью, но этот подход сопряжен со значительными рисками. Манипуляции с указателями подвержены ошибкам, таким как переполнение, доступ по указателю на null, переполнение буфера и, конечно, висячие указатели. Если баг затрагивает только стек или области статической памяти, то он не особо опасен; но если он вдруг затронет критические области системной памяти, это может привести к сбою нашего приложения. И здесь к нам на помощь спешат Span<T> и Memory<T>.

Span<T> и Memory<T> появились в .NET недавно. Они обеспечивают типобезопасный способ доступа к непрерывным областям произвольной памяти. И Span<T> и Memory<T> являются частью пространства имен System и представляют непрерывный блок памяти без какой-либо семантики копирования. Span<T>, Memory<T>, ReadOnlySpan и ReadOnlyMemory были добавлены в C# недавно с целью помочь вам работать с памятью напрямую безопасно и эффективно.

Эти новые типы являются частью System.Memory и предназначены для использования в сценариях, где высокая производительность является критическим показателем, когда необходимо обрабатывать большие объемы данных или по максимуму избегать излишних аллокаций памяти, как, например, при работе с буферами. В отличие от массивов, которые выделяют память в куче со сборщиком мусора, эти новые типы обеспечивают абстракцию над непрерывными (смежными) областями произвольной управляемой или нативной памяти, избегая аллокации в куче со сборщиком мусора.

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

Пара слов о Span

Span<T> (ранее известный как Slice) — это значимый тип, представленный в C# 7.2 и .NET Core 2.1, который минимизирует накладными расходы на работу с памятью. Он обеспечивает типобезопасный способ работы с непрерывными блоками памяти, такими как:

  • Массивы и подмассивы

  • Строки и подстроки

  • Буферы неуправляемой памяти

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

Вот несколько характеристик Span<T> :

  • Значимый тип

  • Низкие или близкие к нулю накладные расходы

  • Высокая производительность

  • Обеспечивает безопасность памяти и типов

Вы можете использовать Span с любым из следующих типов:

  • Массивы

  • Строки

  • Нативные буферы

Список типов, которые можно преобразовать в Span <T>:

  • Массивы

  • Указатели

  • IntPtr

  • stackalloc

Вы можете преобразовать в ReadOnlySpan<T> все из нижеперечисленного:

  • Массивы

  • Указатели

  • IntPtr

  • stackalloc

  • Строки

Span<T> — это чисто стековый тип; точнее, это ByRef тип. Таким образом, спаны (диапазоны/интервалы) не могут быть ни упакованы, ни отображаться как поля стекового типа, ни использоваться в качестве параметров генериков. Однако вы можете использовать спаны для представления возвращаемых значений или аргументов метода. Фрагмент кода, приведенный ниже, иллюстрирует полный исходный код структуры Span:

public readonly ref struct Span<T> 
{
    internal readonly
    ByReference<T> _pointer;
    private readonly int _length;
    // Остальные функции-члены
}

Полный исходный код Span<T> можно найти здесь.

Глядя на исходный код Span<T> мы видим, что он в целом состоит из двух полей, доступных только для чтения: нативного указателя и свойства length отражающего количество элементов, содержащихся в Span.

Span можно использовать так же, как и массив. Однако, в отличие от массивов, он может ссылаться на стековую память, т. е. память, выделенную в стеке, управляемой памяти и нативной памяти. Это дает нам простой способ воспользоваться улучшениями производительности, которые ранее были доступны только при работе с неуправляемым кодом.

Вот как Span<T> объявляется в пространстве имен System:

public readonly ref struct Span<T>

Пустой Span вы можете создать с помощью свойства Span.Empty:

Span<char> span = Span<char>.Empty;

В следующем фрагменте кода продемонстрировано, как можно создать массив байтов в управляемой памяти, а затем создать из него инстанс спана.

var array = new byte[100];
var span = new Span<byte>(array);

Работа со Span в C#

Выделить фрагмент памяти в стеке и заставить Span на него указывать можно следующим образом:

Span<byte> span = stackalloc byte[100];

В следующем фрагменте кода показано, как создать Span на основе массива байтов, сохранить целые числа внутри этого массива, а затем вычислить сумму всех сохраненных целых чисел.

var array = new byte[100];
var span = new Span<byte>(array);

byte data = 0;
for (int index = 0; index < span.Length; index++)
    span[index] = data++;

int sum = 0;
foreach (int value in array)
    sum += value;

Следующий фрагмент кода создает Span из нативной памяти:

var nativeMemory = Marshal.AllocHGlobal(100);
Span<byte> span;
unsafe
{
    span = new Span<byte>(nativeMemory.ToPointer(), 100);
}

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

byte data = 0;
for (int index = 0; index < span.Length; index++)
    span[index] = data++;

int sum = 0;
foreach (int value in span) 
    sum += value;

Console.WriteLine ($"The sum of the numbers in the array is {sum}");
Marshal.FreeHGlobal(nativeMemory);

Вы также можете выделить Span в стековой памяти, используя stackalloc, как показано ниже:

byte data = 0;
Span<byte> span = stackalloc byte[100];

for (int index = 0; index < span.Length; index++)
    span[index] = data++;

int sum = 0;
foreach (int value in span) 
    sum += value;

Console.WriteLine ($"The sum of the numbers in the array is {sum}");

Не забудьте включить компиляцию небезопасного кода в вашем проекте. Для этого кликните по проекту правой кнопкой мыши, выберите “Properties” и установите флажок “Unsafe code”, как показано на рисунке 1.

Рисунок 1: Включение небезопасной компиляции для вашего проекта, чтобы активировать небезопасный код.
Рисунок 1: Включение небезопасной компиляции для вашего проекта, чтобы активировать небезопасный код.

Span и массивы

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

int[] array = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 } ;
Span<int> slice = new Span<int>(array, 2, 3);

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

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

foreach (int i in slice)
    Console.WriteLine($"{i} ");

Если мы выполним фрагмент кода, приведенный выше, в консоли будут отображены целые числа, которые присутствуют в этом слайсе массива, как показано на рисунке 2.

Span и ReadOnlySpan

Инстанс ReadOnlySpan<T> зачастую используется для ссылки на элементы массива или его фрагмент. В отличие от массивов, ReadOnlySpan<T> могут ссылаться на нативную память, управляемую память или стековую память. И Span<T> и ReadOnlySpan<T> обеспечивают типобезопасное представление непрерывной области памяти. Span<T> обеспечивает доступ для чтения и записи к области памяти, а ReadOnlySpan<T> обеспечивает доступ только для чтения из сегмента памяти, как вы могли догадаться из его названия.

В следующем фрагменте кода показано, как можно использовать ReadOnlySpan для разделения строки на слайсы в C#:

ReadOnlySpan<char> readOnlySpan = "This is a sample data for testing purposes.";
int index = readOnlySpan.IndexOf(' ');
var data = ((index < 0) ?
    readOnlySpan : readOnlySpan.Slice(0, index)).ToArray();

Пара слов о Memory

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

Memory<T> определяется следующим образом:

public struct Memory<T> 
{
    void* _ptr;
    T[]   _array;
    int   _offset;
    int   _length;

    public Span<T> Span => _ptr == null ? new Span<T>(_array, _offset, _length) : new Span<T>(_ptr, _length);
}

В дополнение к Span<T>, Memory<T> обеспечивает безопасное и доступное для слайсинга представление любого непрерывного буфера, будь то массив или строка. В отличие от Span<T>, он не ограничен одним лишь стеком, потому что это не ref-подобный тип. В результате вы можете разместить его в куче, использовать в коллекциях или с async-await, сохранить как поле или упаковать, как и любую другую обычную структуру в C#.

Свойство Span<T> позволяет воспользоваться возможностями эффективной индексации, когда вам нужно изменить или обработать буфер, на который ссылается Memory<T>. Напротив, Memory<T> является более универсальным и высокоуровневым типом, чем Span<T>, с неизменяемым (immutable) аналогом, предоставляющим доступ только для чтения, под названием ReadOnlyMemory<T>.

Хотя и Span<T> и Memory<T> представляют непрерывный фрагмент памяти, в отличие от Span<T>, Memory<T> не является ref struct. Таким образом, в отличие от Span<T>, вы можете резместить Memory<T> в любом месте управляемой кучи. Следовательно, у вас нет тех же ограничений в Memory<T>, как в Span<T>. И вы можете использовать Memory<T> как поле класса, а также в рамках await и yield.

ReadOnlyMemory

Подобно ReadOnlySpan<T>, ReadOnlyMemory<T> представляет доступ только для чтения к непрерывной области памяти, но в отличие от ReadOnlySpan<T>, это не ByRef тип.

Давайте рассмотрим следующую строку, содержащую названия стран, разделенные пробелами.

string countries = "India Belgium Australia USA UK Netherlands";
var countries = ExtractStrings("India Belgium Australia USA UK Netherlands".AsMemory());

Метод ExtractStrings извлечет каждое из перечисленных названий стран, как показано ниже:

public static IEnumerable
<ReadOnlyMemory <char>> ExtractStrings(ReadOnlyMemory<char> c)
{
    int index = 0, length = c.Length;
    for (int i = 0; i < length; i++)
    {
        if (char.IsWhiteSpace(c.Span[i]) || i == length)
        {
            yield return c[index..i];
            index = i + 1;
        }
    }
}

Вы можете вызвать описанный выше метод и отобразить названия стран в окне консоли, используя следующий фрагмент кода:

var data = ExtractStrings(countries.AsMemory());
foreach(var str in data)
    Console.WriteLine(str);

Преимущества Span и Memory

Основным преимуществом использования типов Span и Memory является повышение производительности. Вы можете выделить память в стеке, используя ключевое слово stackalloc, которое выделяет неинициализированный блок, являющийся инстансом типа T[size]. В этом нет необходимости, если ваши данные уже находятся в стеке, но для больших объектов это будет полезно, потому что массивы, выделенные таким образом, существуют только в пределах своей области видимости. Если вы используете массивы, размещенные в куче, вы можете передать их через такой метод, как Slice() и создать их представления без копирования каких-либо данных.

Вот еще несколько преимуществ:

  • Они уменьшают количество аллокаций для сборщика мусора. Они также уменьшают количество копий данных и обеспечивают более эффективный способ работы с несколькими буферами одновременно.

  • Они позволяют писать высокопроизводительный код. Например, если у вас есть большой фрагмент памяти, который нужно разделить на фрагменты поменьше, вы можете использовать Span в качестве представления исходной памяти. Это позволит вашему приложению напрямую обращаться к байтам из исходного буфера без создания копий.

  • Они позволяют вам напрямую обращаться к памяти, не копируя ее. Это может быть особенно полезно при работе с нативными библиотеками или при взаимодействии с другими языками.

  • Они позволяют исключить проверку границ в циклах, где производительность имеет решающее значение (например, криптография или проверка сетевых пакетов).

  • Они позволяют устранить затраты на упаковку и распаковку, связанные с универсальными коллекциями, такими как List.

  • Они позволяют писать более понятный код, используя один тип данных (Span), вместо двух (Array и ArraySegment).

Непрерывные и несмежные буферы памяти

Непрерывный (contiguous) буфер памяти — это блок памяти, который содержит данные в последовательно соседних местах. Другими словами, все байты в памяти расположены рядом друг с другом. Массив представляет собой непрерывный буфер памяти. Например:

int[] values = new int[5];

Пять целых чисел в приведенном выше примере будут помещены в пять последовательных ячеек памяти, начиная с первого элемента (values[0]).

В отличие от непрерывных буферов, несмежные (non-contiguous или не непрерывные) буферы можно использовать в случаях, когда у нас имеется несколько блоков данных, не расположенных рядом друг с другом, или при работе с неуправляемым кодом. Типы Span и Memory были разработаны специально для несмежных буферов, обеспечивая удобные способы работы с ними.

Несмежная область памяти не гарантирует, что элементы будут храниться в каком-то определенном порядке или что они будут храниться в памяти близко друг к другу. Несмежные буферы, такие как ReadOnlySequence (при использовании с сегментами), находятся в отдельных областях памяти, которые могут быть разбросаны по куче и недоступны с помощью одного указателя.

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

Несмежные буферы: ReadOnlySequence

Предположим, вы работаете с несмежным буфером. Например, данные могут поступать из сетевого потока, вызовов базы данных или файлового потока. Каждый из этих сценариев может иметь несколько буферов разного размера. Один ReadOnlySequence может содержать один или несколько сегментов памяти, и каждый сегмент может иметь свой собственный инстанс Memory. Таким образом, один ReadOnlySequence позволяет лучше управлять доступной памятью и обеспечивает более высокую производительность, чем множество объединенных инстансов Memory.

Вы можете создать ReadOnlySequence, используя фабричный метод Create() класса SequenceReader, а также другие методы, такие как AsReadOnlySequence(). Метод Create() имеет несколько перегрузок, которые позволяют передавать byte[] или ArraySegment, последовательность байтовых  массивов (IEnumerable) или коллекции IReadOnlyCollection/IReadOnlyList/IList/ICollection байтовых массивов (byte[]) и ArraySegment.

Span<T> и Memory<T> обеспечивают поддержку непрерывных буферов памяти, таких как массивы. Пространство имен System.Buffers содержит структуру ReadOnlySequence<T>, обеспечивающую поддержку работы с несмежными буферами памяти. В следующем фрагменте кода показано, как можно работать с ReadOnlySequence<T> в C#:

int[] array = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
var readOnlySequence = new ReadOnlySequence<int>(array);
var slicedReadOnlySequence = readOnlySequence.Slice(1, 5);

Вы также можете использовать ReadOnlyMemory<T>, как показано ниже:

int[] array = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
ReadOnlyMemory<int> memory = array;
var readOnlySequence = new ReadOnlySequence<int>(memory);
var slicedReadOnlySequence = readOnlySequence.Slice(1,  5);

Реальный практический пример

Теперь давайте рассмотрим реальную задачу и то, как Span<T> и Memory<T> могут здесь помочь. Рассмотрим следующий массив строк, содержащий логовые данные, извлеченные из файла с логами:

string[] logs = new string[]
{
    "a1K3vlCTZE6GAtNYNAi5Vg::05/12/2022 09:10:00 AM::http://localhost:2923/api/customers/getallcustomers",
    "mpO58LssO0uf8Ced1WtAvA::05/12/2022 09:15:00 AM::http://localhost:2923/api/products/getallproducts",
    "2KW1SfJOMkShcdeO54t1TA::05/12/2022 10:25:00 AM::http://localhost:2923/api/orders/getallorders",
    "x5LmCTwMH0isd1wiA8gxIw::05/12/2022 11:05:00 AM::http://localhost:2923/api/orders/getallorders",
    "7IftPSBfCESNh4LD9yI6aw::05/12/2022 11:40:00 AM::http://localhost:2923/api/products/getallproducts"
};

Учтите, что у вас могут быть миллионы логовых записей, поэтому производительность тут имеет решающее значение. Этот пример представляет собой просто извлечение конкретных логов из огромных объемов логовых данных. Данные для каждой из строк включают id HTTP-запроса, дату и время его совершения и URL конечной точки. Теперь предположим, что вам нужно извлечь из этих данных id запроса и URL конечной точки.

Вам нужно решение с высокой производительностью. Если вы используете метод String Substring, то в результате будет создано много строковых объектов, что значительно ухудшит производительность вашего приложения. Лучшее решение — использовать Span<T>, чтобы избежать этих аллокаций. Решением этой проблемы заключается в использовании Span<T> и Slice, как показано в следующем разделе.

Сравнительный анализ производительности

Пришло время провести некоторые измерения. Давайте теперь сравним производительность структуры Span<T> с методом Substring из String.

Создание нового проекта консольного приложения в Visual Studio 2022

Давайте создадим проект консольного приложения, который мы будем использовать для оценки производительности. Вы можете создать проект в Visual Studio 2022 несколькими способами. При запуске Visual Studio 2022 вы увидите окно “Start”. Вы можете выбрать “Continue without code”, чтобы открыть главный экран Visual Studio 2022.

Чтобы создать новый проект консольного приложения в Visual Studio 2022:

  1. Запустите Visual Studio 2022.

  2. В окне “Create a new project” выберите “Console App” и нажмите “Next”, чтобы перейти дальше.

  3. Укажите имя проекта (“HighPerformanceCodeDemo”) и путь, по которому он должен располагаться, в окне “Configure your new project”.

  4. Если вы хотите, чтобы файл солюшена и проект были созданы в одном каталоге, вы можете дополнительно установить флажок “Place solution and project in the same directory”. Нажмите “Next”, чтобы перейти дальше.

  5. На следующем экране укажите целевую платформу, которую вы хотите использовать для своего консольного приложения.

  6. Нажмите “Create”, чтобы завершить процесс создания проекта.

Это приложение пригодится нам в следующих разделах этой статьи.

Установка необходимых NuGet-пакетов

Пока ничего сложного. Следующим шагом является установка необходимых NuGet-пакетов. Чтобы установить необходимые пакеты в свой проект, кликните солюшн правой кнопкой мыши и выберите “Manage NuGet Packages for Solution…”. Теперь найдите пакет с именем “BenchmarkDotNet” и установите его. В качестве альтернативы, вы можете ввести приведенные ниже команды в командной строке диспетчера NuGet-пакетов:

PM> Install-Package BenchmarkDotNet

Оценка производительности Span

Теперь мы наконец можем перейти к сравнительной оценке производительности методов Substring и Slice. Создайте новый класс с именем BenchmarkPerformance, используя код из листинга 1. Вам следует обратить внимание на то, как данные были настроены в методе GlobalSetup и на использование атрибута GlobalSetup.

Листинг 1: Подготовка контрольных данных

[MemoryDiagnoser]
[Orderer(BenchmarkDotNet.Order.SummaryOrderPolicy.FastestToSlowest)]
[RankColumn]

public class BenchmarkPerformance
{
    [Params(100, 200)]
    public int N;

    string countries = null;
    int index, numberOfCharactersToExtract;

    [GlobalSetup]
    public void GlobalSetup()
    {
        countries = "India, USA, UK, Australia, Netherlands, Belgium";
        index = countries.LastIndexOf(",",StringComparison.Ordinal);
        numberOfCharactersToExtract = countries.Length - index;
    }
}

Теперь добавьте два метода с именами Substring и Span, как показано в листинге 2. Первый извлекает последнее название страны с помощью метода String Substring, а второй делает это с помощью Slice .

Листинг 2: Методы Substring и Span

[Benchmark]
public void Substring()
{
    for(int i = 0; i < N; i++)
    {
        var data = countries.Substring(index + 1, numberOfCharactersToExtract - 1);
    }
}

[Benchmark(Baseline = true)]
public void Span()
{
    for(int i=0; i < N; i++)
    {
       var data = countries.AsSpan().Slice(index + 1, numberOfCharactersToExtract - 1);
    }
}

Полный исходный код BenchmarkPerformance приведен для справки в листинге 3.

Листинг 3: Полный исходный код

[MemoryDiagnoser]
[Orderer(BenchmarkDotNet.Order.SummaryOrderPolicy.FastestToSlowest)]
[RankColumn]

public class BenchmarkPerformance
{
    [Params(100, 200)]
    public int N;

    string countries = null;
    int index, numberOfCharactersToExtract;

    [GlobalSetup]
    public void GlobalSetup()
    {
        countries = "India, USA, UK, Australia, Netherlands, Belgium";
        index = countries.LastIndexOf(",",StringComparison.Ordinal);
        numberOfCharactersToExtract = countries.Length - index;
    }

    [Benchmark]
    public void Substring()
    {
        for(int i = 0; i < N; i++)
        {
            var data = countries.Substring(index + 1, numberOfCharactersToExtract - 1);
        }
    }

    [Benchmark(Baseline = true)]
    public void Span()
    {
        for(int i=0; i < N; i++)
        {
            var data = countries.AsSpan().Slice(index + 1, numberOfCharactersToExtract - 1);
        }
    }
}

Бенчмарк

Для запуска бенчмарков добавьте следующий фрагмент кода в Program.cs:

using HighPerformanceCodeDemo;
using System.Runtime.InteropServices;
class Program
{
    static void Main(string[] args)
    {
        BenchmarkRunner.Run<BenchmarkPerformance>();
    }
}

Для выполнения бенчмарков установите для проекта режим компиляции Release и выполните следующую команду в той же папке, где находится файл проекта:

dotnet run -p HighPerformanceCodeDemo.csproj -c Release

На рисунке 3 показан результат проведения бенчмарков.

Рисунок 3: Сравнительный анализ производительности Span (Slice) и Substring
Рисунок 3: Сравнительный анализ производительности Span (Slice) и Substring

Интерпретация результатов сравнительного анализа

Как видно на рисунке 3, когда вы используете Slice для извлечения строки, дополнительные аллокации отсутствуют. Для каждого из тестируемых методов генерируется строка с результатами. Поскольку мы проводим бенчмарки двух методов, то мы видим две строки с данными результатов. Результаты бенчмарков показывают среднее время выполнения, коллекции Gen0 и выделенную память. Как видно из результатов, Span работает более чем в 7.5 раз быстрее, чем Substring .

Ограничения Span

Span<T> исключительно стековый, что означает, что он не подходит для хранения ссылок на буферы в куче, как в подпрограммах, выполняющих асинхронные вызовы. Он размещается не в управляемой куче, а в стеке, и его нельзя упаковать, что предотвращает продвижение в управляемую кучу. Вы не можете использовать Span<T> как универсальный тип, но вы можете использовать его как тип поля в ref struct. Вы не можете назначать Span<T> переменным типа dynamic, object или любого другого типа интерфейса. Вы не можете использовать Span<T> в качестве полей в ссылочном типе, а также не можете использовать его в await и yield конструкциях. Кроме того, поскольку Span<T> не наследует IEnumerable, с ним нельзя использовать LINQ.

Важно отметить, что вы не можете иметь Span<T> поле в классе, создавать массив Span<T> или упаковать инстанс Span<T>. Обратите внимание, что ни Span<T> ни Memory<T> не реализуют IEnumerable<T>. Таким образом, вы не сможете использовать LINQ-операции ни с одним из них. Однако вы можете воспользоваться преимуществами SpanLinq или NetFabric.Hyperlinq, чтобы обойти это ограничение.

Заключение

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


В январе состоится открытое занятие, на котором познакомимся с подмножеством поведенческих шаблонов проектирования GOF, такими как: Стратегия, Итератор и Посредник. Разберем их смысл, правила применения и особенности реализации на языке программирования C#. Регистрация доступна по ссылке для всех желающих.

Комментарии (4)


  1. a-tk
    28.12.2022 14:59
    +1

    Очередная статья из прошлого. На этот раз всего 5 лет.


  1. nikita_dol
    28.12.2022 16:35
    +1

    Не пишу на C#, но есть вопросы:

    1. В бенчмарке, в переменной data, одинаковый результат?

    2. Что, если в тексте будет составной символ? Например, ????‍????‍????‍????


  1. dyadyaSerezha
    29.12.2022 07:36

    Как видно из результатов, Span работает более чем в 7.5 раз быстрее, чем Substring

    Точнее, AsSpan() + Span.Slice().

    Интересно, если в строке тестового метода Span()

    var data = countries.AsSpan().Slice(index + 1, numberOfCharactersToExtract - 1);

    вынести countries.AsSpan() за цикл, насколько повысится производительность?

    В реальных приложениях разница в производительности должна быть еще больше, поскольу в тесте не работает, как я понял, сборщик мусора.


    1. mayorovp
      29.12.2022 08:19

      Проверить не помешает, но, как я думаю, вызов AsSpan() не должен быть сильно дорогим.