Почему этот пример с добавлением массива SIMD может не демонстрировать никакого прироста производительности по сравнению с простой реализацией?

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(Vector.IsHardwareAccelerated ? "SIMD supported" : "SIMD not supported.");

        var rand = new Random();

        var numNums = 10000000;
        var arr1 = Enumerable.Repeat(0, numNums).Select(x => (int) (rand.NextDouble() * 100)).ToArray();
        var arr2 = Enumerable.Repeat(0, numNums).Select(x => (int) (rand.NextDouble() * 100)).ToArray();

        var simdResult = new int [numNums];
        var conventionalResult = new int [numNums];

        var watch = System.Diagnostics.Stopwatch.StartNew();
        ConventionalArrayAddition(arr1, arr2, conventionalResult);
        watch.Stop();
        Console.WriteLine("Conventional time :" + watch.ElapsedMilliseconds);

        var watch2 = System.Diagnostics.Stopwatch.StartNew();
        SIMDArrayAddition(arr1, arr2, simdResult);
        watch2.Stop();
        Console.WriteLine("Simd time :" + watch2.ElapsedMilliseconds);

        Console.ReadKey();
    }

    public static void SIMDArrayAddition(int[] lhs, int[] rhs, int [] result)
    {
        var simdLength = Vector<int>.Count;
        var i = 0;
        for (; i <= lhs.Length - simdLength; i += simdLength)
        {
            var va = new Vector<int>(lhs, i);
            var vb = new Vector<int>(rhs, i);
            (va + vb).CopyTo(result, i);
        }

        for (; i < lhs.Length; ++i)
        {
            result[i] = lhs[i] + rhs[i];
        }
    }

    public static void ConventionalArrayAddition(int[] lhs, int[] rhs, int[] result)
    {
        for (int i = 0; i < lhs.Length; i ++)
        {
            result[i] = lhs[i] + rhs[i];
        }
    }
}

Этот код адаптирован из одного из примеров на https://instil.co/2016/03/21/parallelism-on-a-single-core-simd-with-c/.

Я компилирую это как консольное приложение .Net Framework (я пробовал 4.6.1 и 4.7) с выбранным «Оптимизировать код» как x64.

Результаты, которые я получаю, соответствуют строкам:

Обычное время :22
Симд время :23

Если я выполняю аналогичный тест в ядре .net, я получаю более быстрые результаты, используя векторный метод, но только потому, что наивная реализация намного медленнее в ядре .net (занимает около 55 мс). Векторизованная реализация в ядре обычно немного медленнее (скажем, 24 мс), чем результаты, которые я получаю в .net framework.

Мой процессор i5-7500T, и я получил аналогичные результаты на i5-7200.

Вероятно, есть какая-то другая простая настройка, которой я пренебрегаю? Или, может быть, компилятор каким-то образом оптимизирует использование simd-инструкций в наивном коде?

ОБНОВЛЕНИЕ: следуйте инструкциям в https://blogs.msdn.microsoft.com/clrcodegeneration/2007/10/19/how-to-see-the-assembly-code-generated-by-the-jit.-using-visual-studio/, вот разборка для ConventionalArrayAddition():

            for (int i = 0; i < lhs.Length; i++)
00000000  sub         rsp,28h 
00000004  xor         eax,eax 
00000006  mov         r9d,dword ptr [rcx+8] 
0000000a  test        r9d,r9d 
0000000d  jle         000000000000008A 
0000000f  test        rdx,rdx 
00000012  setne       r10b 
00000016  movzx       r10d,r10b 
0000001a  and         r10d,1 
0000001e  test        r8,r8 
00000021  setne       r11b 
00000025  movzx       r11d,r11b 
00000029  test        r11d,r10d 
0000002c  je          0000000000000066 
0000002e  cmp         dword ptr [rdx+8],r9d 
00000032  setge       r10b 
00000036  movzx       r10d,r10b 
0000003a  cmp         dword ptr [r8+8],r9d 
0000003e  setge       r11b 
00000042  movzx       r11d,r11b 
00000046  test        r11d,r10d 
00000049  je          0000000000000066 
            {
                result[i] = lhs[i] + rhs[i];
0000004b  movsxd      r10,eax 
0000004e  mov         r11d,dword ptr [rcx+r10*4+10h] 
00000053  add         r11d,dword ptr [rdx+r10*4+10h] 
00000058  mov         dword ptr [r8+r10*4+10h],r11d 
            for (int i = 0; i < lhs.Length; i++)
0000005d  inc         eax 
0000005f  cmp         r9d,eax 
00000062  jg          000000000000004B 
00000064  jmp         000000000000008A 
00000066  movsxd      r10,eax 
00000069  mov         r11d,dword ptr [rcx+r10*4+10h] 
0000006e  cmp         eax,dword ptr [rdx+8] 
00000071  jae         000000000000008F 
00000073  add         r11d,dword ptr [rdx+r10*4+10h] 
00000078  cmp         eax,dword ptr [r8+8] 
0000007c  jae         000000000000008F 
0000007e  mov         dword ptr [r8+r10*4+10h],r11d 
00000083  inc         eax 
00000085  cmp         r9d,eax 
00000088  jg          0000000000000066 
0000008a  add         rsp,28h 
            }
        }
0000008e  ret 
0000008f  call        000000005FA91300 
00000094  int         3 

и для SIMDarrayAddition():

    var simdLength = Vector<int>.Count;
00000000  push        rdi 
00000001  push        rsi 
00000002  sub         rsp,28h 
00000006  vzeroupper 
00000009  xor         eax,eax 
            for (; i <= lhs.Length - simdLength; i += simdLength)
0000000b  mov         r9d,dword ptr [rcx+8] 
0000000f  mov         r10d,r9d 
00000012  sub         r10d,8 
00000016  test        r10d,r10d 
00000019  jl          0000000000000064 
0000001b  mov         r11d,dword ptr [rdx+8] 
0000001f  mov         esi,dword ptr [r8+8] 
00000023  cmp         eax,r9d 
00000026  jae         00000000000000A2 
00000028  lea         edi,[rax+7] 
0000002b  cmp         edi,r9d 
0000002e  jae         00000000000000A2 
00000030  vmovupd     ymm0,ymmword ptr [rcx+rax*4+10h] 
                var vb = new Vector<int>(rhs, i);
00000037  cmp         eax,r11d 
0000003a  jae         00000000000000A2 
0000003c  cmp         edi,r11d 
0000003f  jae         00000000000000A2 
00000041  vmovupd     ymm1,ymmword ptr [rdx+rax*4+10h] 
                (va + vb).CopyTo(result, i);
00000048  vpaddd      ymm0,ymm0,ymm1 
0000004d  cmp         eax,esi 
0000004f  jae         00000000000000A7 
00000051  cmp         edi,esi 
00000053  jae         00000000000000AC 
00000055  vmovupd     ymmword ptr [r8+rax*4+10h],ymm0 
            for (; i <= lhs.Length - simdLength; i += simdLength)
0000005c  add         eax,8 
0000005f  cmp         r10d,eax 
00000062  jge         0000000000000023 
            }

            for (; i < lhs.Length; ++i)
00000064  cmp         r9d,eax 
00000067  jle         0000000000000098 
00000069  mov         r11d,dword ptr [rdx+8] 
0000006d  mov         esi,dword ptr [r8+8] 
            {
                result[i] = lhs[i] + rhs[i];
00000071  cmp         eax,r9d 
00000074  jae         00000000000000A2 
00000076  movsxd      r10,eax 
00000079  mov         edi,dword ptr [rcx+r10*4+10h] 
0000007e  cmp         eax,r11d 
00000081  jae         00000000000000A2 
00000083  add         edi,dword ptr [rdx+r10*4+10h] 
00000088  cmp         eax,esi 
0000008a  jae         00000000000000A2 
0000008c  mov         dword ptr [r8+r10*4+10h],edi 
            for (; i < lhs.Length; ++i)
00000091  inc         eax 
00000093  cmp         r9d,eax 
00000096  jg          0000000000000071 
00000098  vzeroupper 
            }
        }
0000009b  add         rsp,28h 
0000009f  pop         rsi 
000000a0  pop         rdi 
000000a1  ret 
000000a2  call        000000005FA91250 
000000a7  call        000000005FA91B00 
000000ac  call        000000005FA91A50 
000000b1  int         3 

Они были получены с другой машины (i7-4790), которая производит аналогичные тайминги.


person topo Reinstate Monica    schedule 27.07.2018    source источник
comment
Существует множество примеров для получения фактической сборки, сгенерированной JIT. Возможно, вы могли бы сделать это и добавить этот код к своему вопросу?   -  person Damien_The_Unbeliever    schedule 27.07.2018
comment
В коде, который вы разместили внутри метода SIMDArrayAddition, у вас есть оба цикла: `simd` и conventional.   -  person Alessandro D'Andria    schedule 27.07.2018
comment
@AlessandroD'Andria, это только для очень немногих оставшихся дополнений, которые не вписываются в точное число, кратное размеру вектора (i не повторно инициализируется).   -  person topo Reinstate Monica    schedule 27.07.2018
comment
@Damien_The_Unbeliever обновлен.   -  person topo Reinstate Monica    schedule 27.07.2018
comment
Тест не работает, вы не измеряете то, что вы думаете. В измерениях преобладают накладные расходы на джиттинг, затраты на ошибки страниц для выделения ОЗУ для массивов и пропускную способность шины памяти. Избавьтесь от первых двух, зациклив тест так, чтобы он повторялся не менее 10 раз. От шины памяти сложнее избавиться, массивы слишком велики, чтобы поместиться в кеши процессора, поэтому программа увязает в ожидании медленной оперативной памяти. Измените numNums, скажем, на 1000 и отобразите Elapsed вместо ElapsedMilliseconds. Я вижу ускорение ~ 40%.   -  person Hans Passant    schedule 27.07.2018
comment
@HansPassant - второй пример кода ассемблера включает инструкцию vpaddd.   -  person Damien_The_Unbeliever    schedule 27.07.2018
comment
@HansPassant да, вместо таких огромных массивов я пытался добавлять крошечные массивы (всего 8 значений) большое количество раз. Это показало примерно 100% ускорение.   -  person topo Reinstate Monica    schedule 27.07.2018
comment
Если ускорение на 100% означает, что оно было в два раза быстрее, то я увидел ускорение на ~80%. Нет большой разницы, когда я использую 8, но будьте осторожны, становится очень сложно надежно измерить такой очень быстрый код. Ваш процессор на 4 года моложе моего (у меня Haswell), так что сравнивать немного сомнительно.   -  person Hans Passant    schedule 27.07.2018
comment
@HansPassant Я новичок в методах оптимизации, поэтому у меня нет хорошего представления о том, каковы относительные скорости вещей, где могут быть накладные расходы и т. Д. Я думаю, что ваш комментарий представляет собой точку обучения, в которой я нуждался, спасибо.   -  person topo Reinstate Monica    schedule 27.07.2018
comment
@HansPassant как ни странно, если я запускаю .exe напрямую из \bin\Release, «оптимизированная» версия примерно в два раза медленнее... для вас это то же самое? (если у вас все еще есть код под рукой)   -  person topo Reinstate Monica    schedule 27.07.2018
comment
Нет, все в порядке. У меня нет подходящей теории на этот счет.   -  person Hans Passant    schedule 27.07.2018


Ответы (1)


Изменение реализации на AddTo для уменьшения количества источников и мест назначения повышает производительность примерно на 70%. Это дополнение полезно во многих случаях, и как работает большинство внутренних дополнений ЦП, уменьшая пропускную способность памяти и требования к кешу.

    public static void SIMDArrayAddTo(int[] lhs, int[] rhs)
    {
        var simdLength = Vector<int>.Count;
        var end = lhs.Length - simdLength;
        var i = 0;
        for (; i <= end; i += simdLength)
        {
            var va = new Vector<int>(lhs, i);
            var vb = new Vector<int>(rhs, i);
            (va + vb).CopyTo(lhs, i);
        }

        for (; i < lhs.Length; ++i)
        {
            lhs[i] += rhs[i];
        }
    }

Я также пытался развернуть цикл SSE, но это, похоже, не помогло. В пакет nuget HPCsharp добавлена ​​версия, аналогичная этой, включая многоядерную версию.

Кроме того, поверх вышеуказанной функции добавлен многоядерный параллелизм, что не улучшило производительность. Если у кого-то есть доступ к процессору с более чем двумя каналами памяти, было бы интересно посмотреть, как этот код масштабируется, когда доступна большая пропускная способность системной памяти.

person DragonSpit    schedule 15.07.2019
comment
связанный: на современных двухъядерных / четырехъядерных процессорах Intel одно ядро ​​​​может почти насытить пропускную способность памяти. На многоядерных процессорах Xeon пропускная способность одного ядра ниже (более высокая задержка для L3 и DRAM ограничивает пропускную способность для того же максимального параллелизма незавершенных кэш-промахов), а совокупная пропускная способность выше. Почему Skylake намного лучше, чем Broadwell-E для однопоточной пропускной способности памяти?. Так что да, для насыщения пропускной способности памяти на процессоре с большим количеством ядер требуется несколько ядер. - person Peter Cordes; 17.07.2019