Обложка канала

.NET Разработчик

Опытный разработчик не так давно зашёл в .Net и поставил цель получить сертификат Microsoft. Свой ежедневный прогресс он описывает на канале .Net Разработчик. Заметки об изученном материале, советы по повышению производительности и поддержке мотивации, ин

.NET Разработчик

3 года назад
Открыть в
День 1535. #ЗаметкиНаПолях LINQ на Стероидах с SIMD В этом посте рассмотрим использование SIMD-инструкций для ускорения запросов LINQ и посмотрим, как это работает совместно с «обобщённой математикой» в C# 10. Допустим, у нас есть список чисел, и мы хотим найти сумму. Самый быстрый способ — использовать SIMD-инструкции (Single Instruction Multiple Data), которые позволяют выполнять одну и ту же операцию с несколькими значениями данных одновременно. Внимание: есть две ловушки, связанные с этим: - Это гораздо сложнее, чем простой цикл или использование LINQ. - Не рекомендуется для небольших наборов данных и когда производительность не критична. Точкой входа в SIMD-операции является тип Vector<T>. T может быть любого числового типа. Тип Vector<T> имеет свойство Count, которое сообщает, сколько элементов он может содержать. Это важно, потому что, если мы, например, складываем два вектора друг с другом, мы делаем это за "одну" операцию. Результатом будет вектор с тем же количеством элементов, что и входные векторы. Чем больше элементов мы можем хранить, тем быстрее будет работать код. Это зависит от вашей архитектуры. В вашем процессоре есть SSE: «Streaming SIMD Extensions» - набор инструкций, позволяющих выполнять SIMD-операции. Размер векторов зависит от версии SSE, которую поддерживает ЦП: SSE2 – 2 элемента, SSE4.1 – 4 и так далее. С появлением C# 10 и «обобщённой математики», мы можем использовать операции SIMD для любого числового типа, не создавая функцию для каждого типа в отдельности. Идея состоит в том, что мы разбиваем нашу проблему на более мелкие подзадачи, которые будут решаться за одну операцию. Например, функция Min: разбиваем весь массив на мелкие фрагменты и ищем в каждом наименьшее значение.
public static T Min<T>(this Span<T> span)
 where T : unmanaged, IMinMaxValue<T>, INumber<T>

Для простоты мы используем Span<T>. Нам нужна непрерывная память, так как блоки памяти передаются в процессор SIMD, который не может обрабатывать произвольные адреса памяти. - INumber<T> — означает, что тип является числом. - IMinMaxValue<T> — тип имеет минимальное и максимальное значение. - unmanaged - тип является типом значения и не содержит ссылочных типов внутри себя. Таким мы можем использовать stackalloc, и запрещать сценарии, в которых структура со ссылочным типом внутри даёт неожиданные результаты. Сначала создаём вектор с максимальным значением типа и приводим Span непосредственно к вектору:
var spanAsVectors = 
  MemoryMarshal.Cast<T, Vector<T>>(span);
Span<T> vector = 
  stackalloc T[Vector<T>.Count];
vector.Fill(T.MaxValue);
var min = new Vector<T>(vector);

Далее сравниваем каждую запись вектора с каждым другим вектором, чтобы получить минимум:
foreach (var v in spanAsVectors)
  min = Vector.Min(v, min);

В итоге получим вектор с n элементами, один из которых является минимальным. НО! MemoryMarshal.Cast<T, Vector<T>>(span) имеет большой недостаток: если у нас Vector<T>.Count = 4, а всего элементов 9, функция вернёт только 2 вектора. Поэтому мы должны выделить и сравнить «остаток»:
var remain = span.Length % Vector<T>.Count;
if (remain > 0)
{
  Span<T> last = 
   stackalloc T[Vector<T>.Count];
  last.Fill(T.MaxValue);
  span[^remain..].CopyTo(last);
  min = Vector.Min(min, new Vector<T>(last));
}

Теперь получим минимальное значение из вектора:
var minVal = T.MaxValue;
for (var i = 0; i < Vector<T>.Count; i++)
  minVal = T.Min(minVal, min[i]);

return minVal;

Результаты бенчмарка для 100000 случайных чисел float впечатляют:
Method     Mean    Ratio
Linq       67.41us  1.00
For        55.58us  0.82
LinqSIMD   10.78us  0.16

С появлением обобщённой математики мы теперь можем выполнять SIMD-операции для пользовательских типов и для более широкого диапазона типов без использования дополнительного кода. Это огромный шаг вперёд для экосистемы .NET. Код этого примера, а также других Linq функций можно найти в библиотеке LinqSIMDExtensions, которая также доступна в виде пакета nuget. Источник: https://steven-giesel.com/blogPost/faf06188-bae9-484d-804d-a42d58d18cad