Найти в Дзене

Повышаем производительность .net приложения с помощью Span<T>

Производительность — это непросто. Нужно следить за цикломатической сложностью, учитывать структуру хранения данных в хранилище, оптимизировать запросы и уменьшать число сетевых вызовов. .NET — управляемая платформа, за выделением и освобождением памяти следит CLR. И это удобно. Но иногда мы можем не обращать внимания, что выделяется дополнительная память, а это приводит к увеличению нагрузки на GC и проблемам с производительностью. Давайте разберемся, как типы Span<T> и ReadOnlySpan<T> помогают повысить производительность приложения. Что такое Span<T> tl;dr: Span<T> — это ссылка на непрерывную область в памяти. То есть, можно сделать несколько разных ссылок на одну область или части областей без необходимости копировать эти области. Во-первых, стоит понимать, как Span реализован в .NET. Это поможет понять, почему использование Span имеет ограничения, но повышает производительность. Span реализован как ref struct, то есть хранится на стеке и не может быть перемещен в кучу. Внутри Sp
Оглавление

Производительность — это непросто. Нужно следить за цикломатической сложностью, учитывать структуру хранения данных в хранилище, оптимизировать запросы и уменьшать число сетевых вызовов.

.NET — управляемая платформа, за выделением и освобождением памяти следит CLR. И это удобно. Но иногда мы можем не обращать внимания, что выделяется дополнительная память, а это приводит к увеличению нагрузки на GC и проблемам с производительностью. Давайте разберемся, как типы Span<T> и ReadOnlySpan<T> помогают повысить производительность приложения.

Что такое Span<T>

tl;dr: Span<T> — это ссылка на непрерывную область в памяти. То есть, можно сделать несколько разных ссылок на одну область или части областей без необходимости копировать эти области.

Во-первых, стоит понимать, как Span реализован в .NET. Это поможет понять, почему использование Span имеет ограничения, но повышает производительность.

Span реализован как ref struct, то есть хранится на стеке и не может быть перемещен в кучу. Внутри Span упрощенно имеет два поля — ссылку на объект T и длину.

Очень упрощенное внутреннее представление Span
Очень упрощенное внутреннее представление Span

Почему использование Span повышает производительность приложения?

  1. Для создания ссылки на область в памяти не нужно копировать само значение области, как было бы в методах ToArray, ToList или при работе со строками, которые являются неизменяемыми в .net.
  2. Само создание Span является zero-allocation, то есть не создает новых объектов в куче. .Объекты Span не нужно отслеживать и очищать с помощью GC поскольку они хранятся на стеке.
  3. Операции с Span при этом такие же эффективные как с массивом потому что для индексации достаточно сложить адрес объекта и смещение в памяти, как и в случае с обычными массивами.

ReadOnlySpan — это структура, аналогичная Span, которая возвращает индексатор в виде readonly ref T. Это позволяет использовать ReadOnlySpan для представления неизменяемых типов данных вроде строк.

Ограничения при работе с Span

Компилятор размещает объекты ссылочных типов в куче. Поэтому мы не можем использовать Span в качестве полей в ссылочных типах — потому что они объявлены как ref struct и не могут перемещаться в кучу.

Локальные переменные типа Span не могут быть захвачены в лямбда-выражениях потому что это предполагает помещение переменной в кучу, а ref stuct это запрещает.

По похожей причине Span не могут быть объявлены в асинхронных методах — построение автомата исполнения предполагает захват переменной. Это кажется довольно большим ограничением, но на самом деле достаточно изолировать код с Span в отдельные методы и использовать эти методы внутри асинхронных.

Используем ReadOnlySpan вместо String

Преимущество при использовании ReadOnlySpan — это отсутствие лишних копирований строк, которые увеличивают использование памяти и нагрузку на GC.

Например, нам нужно посчитать количество непустых строк в большом тексте. Решение в лоб довольно простое:

-2

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

Метод с использованием Span не аллоцирует новой памяти:

-3

Используем Span вместо коллекций

Между массивами и Span<T> есть неявное преобразование, поэтому можно опустить вызов AsSpan для них:

-4

С списками чуть сложнее — понадобится импортировать пространство имен System.Runtime.InteropServices и использовать тип CollectionMarshal:

-5

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

Вот пример бенчмарка работы со строками. Для метода Slice не важно какого размера строка — новая ссылка на область с подстрокой будет создана за константное время и не приведет к аллокации памяти:

-6

Результаты:

-7

Источники