Pull to refresh

Comments 12

Почему unsafe оказался медленнее? Для меня в этом загадка.

Затраты на fixed? Можно было бы и приложить код

Возможно. Код не приложил, извините - за давностью лет немного потерялся. Попробую восстановить.

А зачем цикл в 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 |

Я 3.1 для себя уже списал, на нём не тестировал. Думаю что разница из-за того что у нас с вами разные процессоры, у которых разная архитектура, в т.ч. Доступа к памяти.

Интересно ещё попробовать в анроллинг (пока у меня нет возможности проверить скорость):

[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 потока.

Ого! Спасибо, я попробую. С SIMD обращусь к специалисту, у меня с ним нет опыта.

Добавил методы 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.

Sign up to leave a comment.

Articles