Comments 12
Почему unsafe оказался медленнее? Для меня в этом загадка.
Затраты на fixed? Можно было бы и приложить код
Возможно. Код не приложил, извините - за давностью лет немного потерялся. Попробую восстановить.
Спасибо большое за замечание! Ссылку обновил - https://gist.github.com/teoadal/9297c0b574a175fc295bb29c01782fa2
А зачем цикл в unsafe по int? Можно высчитать указатель на конечный элемент и сделать цикл по указателю.
[Benchmark]
public unsafe int UnsafeFixed()
{
var sum = 0;
fixed (int* arrayPtr = _array)
{
var ptr = arrayPtr;
var endPtr = arrayPtr + _array.Length;
while (ptr < endPtr)
{
sum += *ptr++;
}
}
return sum;
}
Ожидаемо разрывает всё что угодно:
| Method | Job | Runtime | Mean | Error | StdDev | Ratio |
|------------ |--------------------- |--------------------- |---------:|--------:|--------:|------:|
| For | .NET 6.0 | .NET 6.0 | 409.5 ns | 3.77 ns | 3.53 ns | 1.10 |
| Foreach | .NET 6.0 | .NET 6.0 | 373.2 ns | 0.32 ns | 0.25 ns | 1.00 |
| ForeachSpan | .NET 6.0 | .NET 6.0 | 372.4 ns | 0.42 ns | 0.39 ns | 1.00 |
| Unsafe | .NET 6.0 | .NET 6.0 | 362.7 ns | 2.35 ns | 2.20 ns | 0.97 |
| UnsafeFixed | .NET 6.0 | .NET 6.0 | 316.6 ns | 2.00 ns | 1.77 ns | 0.85 |
| | | | | | | |
| For | .NET Framework 4.7.2 | .NET Framework 4.7.2 | 391.7 ns | 0.22 ns | 0.19 ns | 1.05 |
| Foreach | .NET Framework 4.7.2 | .NET Framework 4.7.2 | 373.4 ns | 0.07 ns | 0.06 ns | 1.00 |
| ForeachSpan | .NET Framework 4.7.2 | .NET Framework 4.7.2 | 544.2 ns | 0.26 ns | 0.23 ns | 1.46 |
| Unsafe | .NET Framework 4.7.2 | .NET Framework 4.7.2 | 364.9 ns | 0.97 ns | 0.91 ns | 0.98 |
| UnsafeFixed | .NET Framework 4.7.2 | .NET Framework 4.7.2 | 317.6 ns | 0.70 ns | 0.62 ns | 0.85 |
Спасибо! Я добавлю ваш комментарий в статью. Однако, я должен отметить, что мои измерения несколько иные. Действительно "рвёт" только в .NET Core 3.1. Код обновил.
Окружение:
BenchmarkDotNet=v0.13.5, OS=Windows 11 (10.0.22621.1265/22H2/2022Update/SunValley2)
AMD Ryzen 7 5800H with Radeon Graphics, 1 CPU, 16 logical and 8 physical cores
.NET SDK=7.0.102
[Host] : .NET 6.0.13 (6.0.1322.58009), X64 RyuJIT AVX2
.NET 6.0 : .NET 6.0.13 (6.0.1322.58009), X64 RyuJIT AVX2
.NET Core 3.1 : .NET Core 3.1.22 (CoreCLR 4.700.21.56803, CoreFX 4.700.21.57101), X64 RyuJIT AVX2
.NET Framework 4.8 : .NET Framework 4.8.1 (4.8.9139.0), X64 RyuJIT VectorSize=256
Результаты:
| Method | Job | Runtime | Mean | Error | StdDev | Ratio | RatioSD |
|-------------- |------------------- |------------------- |---------:|---------:|---------:|------:|--------:|
| For | .NET 6.0 | .NET 6.0 | 504.5 ns | 2.37 ns | 2.22 ns | 2.00 | 0.03 |
| Foreach | .NET 6.0 | .NET 6.0 | 252.3 ns | 3.69 ns | 3.08 ns | 1.00 | 0.00 |
| ForeachCustom | .NET 6.0 | .NET 6.0 | 254.8 ns | 2.63 ns | 2.46 ns | 1.01 | 0.02 |
| ForeachSpan | .NET 6.0 | .NET 6.0 | 252.3 ns | 3.82 ns | 3.39 ns | 1.00 | 0.02 |
| Unsafe | .NET 6.0 | .NET 6.0 | 260.3 ns | 3.01 ns | 2.67 ns | 1.03 | 0.02 |
| UnsafeFixed | .NET 6.0 | .NET 6.0 | 251.6 ns | 3.88 ns | 3.24 ns | 1.00 | 0.02 |
| | | | | | | | |
| For | .NET Core 3.1 | .NET Core 3.1 | 516.3 ns | 10.07 ns | 10.34 ns | 1.02 | 0.03 |
| Foreach | .NET Core 3.1 | .NET Core 3.1 | 505.6 ns | 5.50 ns | 5.14 ns | 1.00 | 0.00 |
| ForeachCustom | .NET Core 3.1 | .NET Core 3.1 | 503.9 ns | 5.28 ns | 4.94 ns | 1.00 | 0.01 |
| ForeachSpan | .NET Core 3.1 | .NET Core 3.1 | 252.4 ns | 2.86 ns | 2.67 ns | 0.50 | 0.01 |
| Unsafe | .NET Core 3.1 | .NET Core 3.1 | 261.4 ns | 2.78 ns | 2.60 ns | 0.52 | 0.01 |
| UnsafeFixed | .NET Core 3.1 | .NET Core 3.1 | 251.8 ns | 2.13 ns | 1.99 ns | 0.50 | 0.01 |
| | | | | | | | |
| For | .NET Framework 4.8 | .NET Framework 4.8 | 506.1 ns | 7.55 ns | 7.07 ns | 1.99 | 0.02 |
| Foreach | .NET Framework 4.8 | .NET Framework 4.8 | 253.7 ns | 1.96 ns | 1.73 ns | 1.00 | 0.00 |
| ForeachCustom | .NET Framework 4.8 | .NET Framework 4.8 | 437.6 ns | 8.57 ns | 8.80 ns | 1.72 | 0.04 |
| ForeachSpan | .NET Framework 4.8 | .NET Framework 4.8 | 760.1 ns | 9.50 ns | 8.89 ns | 3.00 | 0.04 |
| Unsafe | .NET Framework 4.8 | .NET Framework 4.8 | 505.0 ns | 3.33 ns | 2.95 ns | 1.99 | 0.02 |
| UnsafeFixed | .NET Framework 4.8 | .NET Framework 4.8 | 252.2 ns | 2.79 ns | 2.61 ns | 0.99 | 0.01 |
Интересно ещё попробовать в анроллинг (пока у меня нет возможности проверить скорость):
[Benchmark]
public unsafe long UnsafeFixedWithUnroll()
{
int sum = 0;
fixed (int* arrayPtr = _array)
{
var ptr = arrayPtr;
var endPtrFast = arrayPtr + _array.LongLength / 32 * 32;
//Сравнение ptr < endPtrFast происходит в 32 раза реже
while (ptr < endPtrFast)
{
sum += *ptr++;
sum += *ptr++;
sum += *ptr++;
sum += *ptr++;
sum += *ptr++;
sum += *ptr++;
sum += *ptr++;
sum += *ptr++;
sum += *ptr++;
sum += *ptr++;
sum += *ptr++;
sum += *ptr++;
sum += *ptr++;
sum += *ptr++;
sum += *ptr++;
sum += *ptr++;
sum += *ptr++;
sum += *ptr++;
sum += *ptr++;
sum += *ptr++;
sum += *ptr++;
sum += *ptr++;
sum += *ptr++;
sum += *ptr++;
sum += *ptr++;
sum += *ptr++;
sum += *ptr++;
sum += *ptr++;
sum += *ptr++;
sum += *ptr++;
sum += *ptr++;
sum += *ptr++;
}
var endPtr = arrayPtr + _array.LongLength;
//Досчитываем хвостик
while (ptr < endPtr)
sum += *ptr++;
}
return sum;
}
Ещё можно попробовать прикрутить к этому циклу SIMDы через Vector<int>, но тут уже всё зависеть будет от архитектуры ЦП — выигрыш будет не всегда. Ну и банальная многопоточность по всем доступным ядрам, но она будет выгодна начиная с определённого размера массива, т.к. появляются накладные расходы. Однако, можно прямо в алгоритм запихнуть автоматическую адаптацию к конкретной системе и условиям, чтобы алгоритм сам решал, когда запускать многопоточность, а когда достаточно 1 потока.
Добавил методы ForUnsafeRef
и ForUnsafe2Refs
их лучше использовать вместо методов с указателями, потому что ссылки это более современные указатели. Исходники тут.
ForUnsafe2Refs
работает быстрее в .NET 6.0 и младше, в последних версиях .NET 7.0 и старше скорость обычного цикла не отличается. Тут уже компилятор похоже выжимает максимум на целых массивах.
Для других типов данных не тестировал, думаю для больших типов значений (value types) эти методы и особенно ForUnsafe2Refs
будут работать быстрее чем обычный цикл на любой версии .NET, так как не будет выполняться копирование.
Вот их производительность на моем компьютере:
| Method | Job | Runtime | Mean | Error | StdDev | Ratio | RatioSD |
|--------------- |------------------- |------------------- |-----------:|---------:|---------:|------:|--------:|
| ForUnsafeRef | .NET 6.0 | .NET 6.0 | 748.2 ns | 14.15 ns | 21.61 ns | 1.02 | 0.03 |
| ForUnsafe2Refs | .NET 6.0 | .NET 6.0 | 593.9 ns | 11.85 ns | 21.06 ns | 0.80 | 0.04 |
| | | | | | | | |
| ForUnsafeRef | .NET 7.0 | .NET 7.0 | 726.0 ns | 14.54 ns | 13.60 ns | 1.27 | 0.03 |
| ForUnsafe2Refs | .NET 7.0 | .NET 7.0 | 591.5 ns | 11.47 ns | 13.65 ns | 1.04 | 0.03 |
| | | | | | | | |
| ForUnsafeRef | .NET 8.0 | .NET 8.0 | 769.5 ns | 13.36 ns | 12.49 ns | 1.28 | 0.03 |
| ForUnsafe2Refs | .NET 8.0 | .NET 8.0 | 596.1 ns | 11.48 ns | 12.29 ns | 0.99 | 0.03 |
| | | | | | | | |
| ForUnsafeRef | .NET Core 3.1 | .NET Core 3.1 | 741.4 ns | 13.16 ns | 12.31 ns | 0.85 | 0.02 |
| ForUnsafe2Refs | .NET Core 3.1 | .NET Core 3.1 | 595.3 ns | 11.93 ns | 13.26 ns | 0.68 | 0.02 |
| | | | | | | | |
| ForUnsafeRef | .NET Framework 4.8 | .NET Framework 4.8 | 928.3 ns | 9.07 ns | 8.48 ns | 1.18 | 0.03 |
| ForUnsafe2Refs | .NET Framework 4.8 | .NET Framework 4.8 | 618.0 ns | 11.87 ns | 12.70 ns | 0.78 | 0.02
Linq и foreach использует кучу и генерят мусор, это тоже нужно учитывать. Это делает их бесполезными и медленными. При этом оригинальный linq из F# сделан адекватно.
Foreach не генерит мусор, если Enumerator это struct. Вроде как struct enumerator'ы реализованы уже для всех основных коллекций.
Если же вы бежите по IList или другим интерфейсам коллекций, то да, struct enumerator будет упакован и будет аллокация.
Про Linq согласен. Linq в "горячем месте кода" надо разворачивать в foreach.
Array: for/foreach или unsafe