Вычислить средние значения 4d с помощью SSE

Я пытаюсь ускорить вычисление среднего значения 4d векторов, помещенных в массив. Вот мой код:

#include <sys/time.h>
#include <sys/param.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <xmmintrin.h>

typedef float dot[4];
#define N 1000000

double gettime ()
{
    struct timeval tv;
    gettimeofday (&tv, 0);
    return (double)tv.tv_sec + (0.000001 * (double)tv.tv_usec);
}

void calc_avg1 (dot res, const dot array[], int n)
{
    int i,j;
    memset (res, 0, sizeof (dot));
    for (i = 0; i < n; i++)
    {
        for (j = 0; j<4; j++) res[j] += array[i][j];
    }
    for (j = 0; j<4; j++) res[j] /= n;
}

void calc_avg2 (dot res, const dot array[], int n)
{
    int i;
    __v4sf r = _mm_set1_ps (0.0);
    for (i=0; i<n; i++) r += _mm_load_ps (array[i]);
    r /= _mm_set1_ps ((float)n);
    _mm_store_ps (res, r);
}

int main ()
{
    void *space = malloc (N*sizeof(dot)+15);
    dot *array = (dot*)(((unsigned long)space+15) & ~(unsigned long)15);
    dot avg __attribute__((aligned(16)));
    int i;
    double time;

    for (i = 0; i < N; i++)
    {
        array[i][0] = 1.0*random();
        array[i][1] = 1.0*random();
        array[i][2] = 1.0*random();
    }
    time = gettime();
    calc_avg1 (avg, array, N);
    time = gettime() - time;
    printf ("%f\n%f %f %f\n", time, avg[0], avg[1], avg[2]);

    time = gettime();
    calc_avg2 (avg, array, N);
    time = gettime() - time;
    printf ("%f\n%f %f %f\n", time, avg[0], avg[1], avg[2]);
    return 0;
}

Итак, как вы видите, calc_avg1 использует наивные циклы от 0 до 4 и calc_avg2 заменяет их инструкциями SSE. Я компилирую этот код с помощью clang 3.4:

cc -O2 -o test test.c

Здесь разбираются функции calc_avgX:

0000000000400860 <calc_avg1>:
  400860:   55                      push   %rbp
  400861:   48 89 e5                mov    %rsp,%rbp
  400864:   85 d2                   test   %edx,%edx
  400866:   0f 57 c0                xorps  %xmm0,%xmm0
  400869:   0f 11 07                movups %xmm0,(%rdi)
  40086c:   7e 42                   jle    4008b0 <calc_avg1+0x50>
  40086e:   48 83 c6 0c             add    $0xc,%rsi
  400872:   0f 57 c0                xorps  %xmm0,%xmm0
  400875:   89 d0                   mov    %edx,%eax
  400877:   0f 57 c9                xorps  %xmm1,%xmm1
  40087a:   0f 57 d2                xorps  %xmm2,%xmm2
  40087d:   0f 57 db                xorps  %xmm3,%xmm3
  400880:   f3 0f 58 5e f4          addss  -0xc(%rsi),%xmm3
  400885:   f3 0f 11 1f             movss  %xmm3,(%rdi)
  400889:   f3 0f 58 56 f8          addss  -0x8(%rsi),%xmm2
  40088e:   f3 0f 11 57 04          movss  %xmm2,0x4(%rdi)
  400893:   f3 0f 58 4e fc          addss  -0x4(%rsi),%xmm1
  400898:   f3 0f 11 4f 08          movss  %xmm1,0x8(%rdi)
  40089d:   f3 0f 58 06             addss  (%rsi),%xmm0
  4008a1:   f3 0f 11 47 0c          movss  %xmm0,0xc(%rdi)
  4008a6:   48 83 c6 10             add    $0x10,%rsi
  4008aa:   ff c8                   dec    %eax
  4008ac:   75 d2                   jne    400880 <calc_avg1+0x20>
  4008ae:   eb 0c                   jmp    4008bc <calc_avg1+0x5c>
  4008b0:   0f 57 c0                xorps  %xmm0,%xmm0
  4008b3:   0f 57 c9                xorps  %xmm1,%xmm1
  4008b6:   0f 57 d2                xorps  %xmm2,%xmm2
  4008b9:   0f 57 db                xorps  %xmm3,%xmm3
  4008bc:   f3 0f 2a e2             cvtsi2ss %edx,%xmm4
  4008c0:   f3 0f 5e dc             divss  %xmm4,%xmm3
  4008c4:   f3 0f 11 1f             movss  %xmm3,(%rdi)
  4008c8:   f3 0f 5e d4             divss  %xmm4,%xmm2
  4008cc:   f3 0f 11 57 04          movss  %xmm2,0x4(%rdi)
  4008d1:   f3 0f 5e cc             divss  %xmm4,%xmm1
  4008d5:   f3 0f 11 4f 08          movss  %xmm1,0x8(%rdi)
  4008da:   f3 0f 5e c4             divss  %xmm4,%xmm0
  4008de:   f3 0f 11 47 0c          movss  %xmm0,0xc(%rdi)
  4008e3:   5d                      pop    %rbp
  4008e4:   c3                      retq   
  4008e5:   66 66 2e 0f 1f 84 00    nopw   %cs:0x0(%rax,%rax,1)
  4008ec:   00 00 00 00 

00000000004008f0 <calc_avg2>:
  4008f0:   55                      push   %rbp
  4008f1:   48 89 e5                mov    %rsp,%rbp
  4008f4:   85 d2                   test   %edx,%edx
  4008f6:   0f 57 c0                xorps  %xmm0,%xmm0
  4008f9:   7e 10                   jle    40090b <calc_avg2+0x1b>
  4008fb:   89 d0                   mov    %edx,%eax
  4008fd:   0f 1f 00                nopl   (%rax)
  400900:   0f 58 06                addps  (%rsi),%xmm0
  400903:   48 83 c6 10             add    $0x10,%rsi
  400907:   ff c8                   dec    %eax
  400909:   75 f5                   jne    400900 <calc_avg2+0x10>
  40090b:   66 0f 6e ca             movd   %edx,%xmm1
  40090f:   66 0f 70 c9 00          pshufd $0x0,%xmm1,%xmm1
  400914:   0f 5b c9                cvtdq2ps %xmm1,%xmm1
  400917:   0f 5e c1                divps  %xmm1,%xmm0
  40091a:   0f 29 07                movaps %xmm0,(%rdi)
  40091d:   5d                      pop    %rbp
  40091e:   c3                      retq   
  40091f:   90                      nop    

И вот результат:

> ./test
0.004287
1073864320.000000 1074018048.000000 1073044224.000000
0.003661
1073864320.000000 1074018048.000000 1073044224.000000

Таким образом, версия SSE в 1,17 раза быстрее. Но когда я пытаюсь сделать, по-видимому, ту же работу, которая заключается в вычислении среднего числа скаляров одиночной точности в массиве (например, здесь сокращение SSE для флоат-вектора) Версия SSE работает в 3,32 раза быстрее. Вот код функций calc_avgX:

float calc_avg1 (const float array[], int n)
{
    int i;
    float avg = 0;
    for (i = 0; i < n; i++) avg += array[i];
    return avg / n;
}

float calc_avg3 (const float array[], int n)
{
    int i;
    __v4sf r = _mm_set1_ps (0.0);
    for (i=0; i<n; i+=4) r += _mm_load_ps (&(array[i]));
    r = _mm_hadd_ps (r, r);
    r = _mm_hadd_ps (r, r);
    return r[0] / n;
}

Итак, мой вопрос заключается в следующем: почему в последнем примере я пользуюсь SSE так много (вычисление среднего числа одиночных скаляриев float) и не в первом (вычисление среднего значения 4d векторов)? Для меня эти рабочие места почти одинаковы. Каков правильный способ ускорить вычисления в первом примере, если я ошибаюсь?

UPD: Если вы считаете это актуальным, я также предлагаю разборку второго примера, где вычисляется среднее значение скаляров (также скомпилировано с clang3.4-O2).

0000000000400860 <calc_avg1>:
  400860:   55                      push   %rbp
  400861:   48 89 e5                mov    %rsp,%rbp
  400864:   85 d2                   test   %edx,%edx
  400866:   0f 57 c0                xorps  %xmm0,%xmm0
  400869:   0f 11 07                movups %xmm0,(%rdi)
  40086c:   7e 42                   jle    4008b0 <calc_avg1+0x50>
  40086e:   48 83 c6 0c             add    $0xc,%rsi
  400872:   0f 57 c0                xorps  %xmm0,%xmm0
  400875:   89 d0                   mov    %edx,%eax
  400877:   0f 57 c9                xorps  %xmm1,%xmm1
  40087a:   0f 57 d2                xorps  %xmm2,%xmm2
  40087d:   0f 57 db                xorps  %xmm3,%xmm3
  400880:   f3 0f 58 5e f4          addss  -0xc(%rsi),%xmm3
  400885:   f3 0f 11 1f             movss  %xmm3,(%rdi)
  400889:   f3 0f 58 56 f8          addss  -0x8(%rsi),%xmm2
  40088e:   f3 0f 11 57 04          movss  %xmm2,0x4(%rdi)
  400893:   f3 0f 58 4e fc          addss  -0x4(%rsi),%xmm1
  400898:   f3 0f 11 4f 08          movss  %xmm1,0x8(%rdi)
  40089d:   f3 0f 58 06             addss  (%rsi),%xmm0
  4008a1:   f3 0f 11 47 0c          movss  %xmm0,0xc(%rdi)
  4008a6:   48 83 c6 10             add    $0x10,%rsi
  4008aa:   ff c8                   dec    %eax
  4008ac:   75 d2                   jne    400880 <calc_avg1+0x20>
  4008ae:   eb 0c                   jmp    4008bc <calc_avg1+0x5c>
  4008b0:   0f 57 c0                xorps  %xmm0,%xmm0
  4008b3:   0f 57 c9                xorps  %xmm1,%xmm1
  4008b6:   0f 57 d2                xorps  %xmm2,%xmm2
  4008b9:   0f 57 db                xorps  %xmm3,%xmm3
  4008bc:   f3 0f 2a e2             cvtsi2ss %edx,%xmm4
  4008c0:   f3 0f 5e dc             divss  %xmm4,%xmm3
  4008c4:   f3 0f 11 1f             movss  %xmm3,(%rdi)
  4008c8:   f3 0f 5e d4             divss  %xmm4,%xmm2
  4008cc:   f3 0f 11 57 04          movss  %xmm2,0x4(%rdi)
  4008d1:   f3 0f 5e cc             divss  %xmm4,%xmm1
  4008d5:   f3 0f 11 4f 08          movss  %xmm1,0x8(%rdi)
  4008da:   f3 0f 5e c4             divss  %xmm4,%xmm0
  4008de:   f3 0f 11 47 0c          movss  %xmm0,0xc(%rdi)
  4008e3:   5d                      pop    %rbp
  4008e4:   c3                      retq   
  4008e5:   66 66 2e 0f 1f 84 00    nopw   %cs:0x0(%rax,%rax,1)
  4008ec:   00 00 00 00 

00000000004008d0 <calc_avg3>:
  4008d0:   55                      push   %rbp
  4008d1:   48 89 e5                mov    %rsp,%rbp
  4008d4:   31 c0                   xor    %eax,%eax
  4008d6:   85 f6                   test   %esi,%esi
  4008d8:   0f 57 c0                xorps  %xmm0,%xmm0
  4008db:   7e 0f                   jle    4008ec <calc_avg3+0x1c>
  4008dd:   0f 1f 00                nopl   (%rax)
  4008e0:   0f 58 04 87             addps  (%rdi,%rax,4),%xmm0
  4008e4:   48 83 c0 04             add    $0x4,%rax
  4008e8:   39 f0                   cmp    %esi,%eax
  4008ea:   7c f4                   jl     4008e0 <calc_avg3+0x10>
  4008ec:   66 0f 70 c8 01          pshufd $0x1,%xmm0,%xmm1
  4008f1:   f3 0f 58 c8             addss  %xmm0,%xmm1
  4008f5:   66 0f 70 d0 03          pshufd $0x3,%xmm0,%xmm2
  4008fa:   0f 12 c0                movhlps %xmm0,%xmm0
  4008fd:   f3 0f 58 c1             addss  %xmm1,%xmm0
  400901:   f3 0f 58 c2             addss  %xmm2,%xmm0
  400905:   0f 57 c9                xorps  %xmm1,%xmm1
  400908:   f3 0f 2a ce             cvtsi2ss %esi,%xmm1
  40090c:   f3 0f 5e c1             divss  %xmm1,%xmm0
  400910:   5d                      pop    %rbp
  400911:   c3                      retq   
  400912:   66 66 66 66 66 2e 0f    nopw   %cs:0x0(%rax,%rax,1)
  400919:   1f 84 00 00 00 00 00 

Ответ 1

Извините, этот ответ получил немного длинный и бессвязный. Я провел несколько тестов, но я долго не редактировал предыдущие материалы, думая о чем-то другом, чтобы попробовать.

Ваш рабочий набор составляет 15.25MiB (16MB). Обычно для сравнения такой процедуры вы будете усреднять меньший буфер несколько раз, поэтому он вписывается в кеш. Вы не видите большой разницы между медленной версией и быстрой версией, поскольку diff скрывается узким местом памяти.

calc_avg1 не автоиндексирует вообще (обратите внимание, что addss. ss означает скалярную, одинарную точность, в отличие от addps (упакованная одиночная точность)). Я думаю, что он не может autovectorize даже когда он встроен в основной, потому что он не может быть уверен, что в 4-й позиции вектора нет NaN, что вызовет исключение FP, которое не было бы скалярным кодом. Я попытался скомпилировать его для Sandybridge с gcc 4.9.2 -O3 -march=native -ffast-math и с clang-3.5, но не повезло и с.

Тем не менее, версия, встроенная в main, работает только медленнее, потому что память является узким местом. 32-битные нагрузки могут почти соответствовать нагрузкам 128b при попадании в основную память. (Однако нестрочная версия будет плохой: каждый результат += сохраняется в массиве res, потому что цикл накапливается непосредственно в памяти, который может иметь другие ссылки на него. Поэтому он должен сделать каждую операцию видимой с помощью магазин. Это версия, которую вы разместили для разборки, для BTW. Сортировка, какая часть основной была использована при компиляции с помощью -S -fverbose-asm.)

Несколько разочаровывающе, clang и gcc не могут автоматически векторизовать __v4sf от 4-х до 8-х широкополосного AVX.

Поцарапайте, что после обертывания for (int i=0; i<4000 ; i++) вокруг вызовов calc_avgX и уменьшения N до 10k gcc -O3 превращает внутренний внутренний цикл avg1 в:

  400690:       c5 f8 10 08             vmovups (%rax),%xmm1
  400694:       48 83 c0 20             add    $0x20,%rax
  400698:       c4 e3 75 18 48 f0 01    vinsertf128 $0x1,-0x10(%rax),%ymm1,%ymm1
  40069f:       c5 fc 58 c1             vaddps %ymm1,%ymm0,%ymm0
  4006a3:       48 39 d8                cmp    %rbx,%rax
  4006a6:       75 e8                   jne    400690 <main+0xe0>

$ (get CPU to max-turbo frequency) && time ./a.out
0.016515
1071570752.000000 1066917696.000000 1073897344.000000
0.032875
1071570944.000000 1066916416.000000 1073895680.000000

Это bizzare; Я понятия не имею, почему он не просто использует 32B нагрузки. Он использует 32B vaddps, что является узким местом при работе с набором данных, который подходит в кэше L2.

IDK почему ему удалось авто-векторизовать внутренний цикл, когда он находился внутри другого цикла. Обратите внимание, что это относится только к версии, встроенной в main. Вызываемая версия остается только скалярной. Также обратите внимание, что только gcc справился с этим. clang 3.5 не сделал. Может быть, gcc знал, что он будет использовать malloc таким образом, который возвратил нулевой буфер (так что не нужно было беспокоиться о NaN в 4-м элементе)?

Я также удивлен, что clang non-vectorized avg1 не медленнее, когда все вписывается в кеш. N=10000, repeat-count = 40k.

3.3GHz SNB i5 2500k, max turbo = 3.8GHz.
avg1: 0.350422s:  clang -O3 -march=native (not vectorized.  loop of 6 scalar addss with memory operands)
avg2: 0.320173s:  clang -O3 -march=native
avg1: 0.497040s:  clang -O3 -march=native -ffast-math (haven't looked at asm to see what happened)

avg1: 0.160374s:  gcc -O3 -march=native (256b addps, with 2 128b loads)
avg2: 0.321028s:  gcc -O3 -march=native (128b addps with a memory operand)

avg2: ~0.16:  clang, unrolled with 2 dependency chains to hide latency (see below).
avg2: ~0.08:  unrolled with 4 dep chains
avg2: ~0.04:  in theory unrolled-by-4 with 256b AVX.  I didn't try unrolling the one gcc auto-vectorized with 256b addps

Таким образом, большой сюрприз заключается в том, что скалярный код clang avg1 поддерживает avg2. Возможно, цепочка зависимостей, связанная с циклом, является большим узким местом?

perf показывает 1.47 insns за цикл для clang non-vectorized avg1, который, вероятно, насыщает блок добавления FP на порт 1. (Большинство команд цикла добавляются).

Однако avg2, используя 128b addps с операндом памяти, получает только 0,58 insns за цикл. Уменьшение размера массива на другой коэффициент 10, до N=1000, получает 0,60 insns за цикл, вероятно, из-за большего количества времени в прологе/эпилоге. Поэтому я думаю, что серьезная проблема с цепочкой зависимостей, связанной с циклом. clang разворачивает цикл на 4, но использует только один аккумулятор. Цикл имеет 7 инструкций, которые декодируют до 10 ударов. (Каждый vaddps равен 2, так как он используется с операндом памяти с режимом адресации с двумя регистрами, предотвращающим микроплавление. Макро-предохранитель cmp и jne). http://www.brendangregg.com/perf.html говорит, что событие perf для UOPS_DISPATCHED.CORE равно r2b1, поэтому:

$ perf stat -d -e cycles,instructions,r2b1 ./a.out
0.031793
1053298112.000000 1052673664.000000 1116960256.000000

 Performance counter stats for './a.out':

       118,453,541      cycles
        71,181,299      instructions              #    0.60  insns per cycle
       102,025,443      r2b1  # this is uops, but perf doesn't have a nice name for it
        40,256,019      L1-dcache-loads
            21,254      L1-dcache-load-misses     #    0.05% of all L1-dcache hits
             9,588      LLC-loads
                 0      LLC-load-misses:HG        #    0.00% of all LL-cache hits

       0.032276233 seconds time elapsed

Это подтверждает мои инструкции 7:10 для анализа uops. Это фактически не имеет отношения к проблеме производительности здесь: цикл работает медленнее, чем верхний предел 4 мкп за цикл. Изменение внутреннего цикла для получения двух отдельных цепочек депиляции удваивает пропускную способность (60M циклов вместо 117M, но 81M insns вместо 71M):

for (i=0; i<n-1; i+=2) {  // TODO: make sure the loop end condition is correct
   r0 += _mm_load_ps (array[i]);
   r1 += _mm_load_ps (array[i+1]);
}
r0 += r1;

Развертывание на 4 (с 4 аккумуляторами, которые вы сливаете в конце цикла) снова удваивает производительность. (до 42M циклов, 81M insns, 112M uops.) Внутренний цикл имеет 4x vaddps -0x30(%rcx),%xmm4,%xmm4 (и аналогичный), 2x add, cmp, jl. Эта форма vaddps должна быть микро-предохранителем, но я все еще вижу намного больше, чем инструкций, поэтому, думаю, r2b1 подсчитывает unused-domain uops. (Linux perf не имеет хороших документов для специфичных для платформы событий HW). Снова переверните N, чтобы убедиться, что самая внутренняя петля полностью доминирует во всех подсчетах, я вижу отношение uop: insn 1.39, которое хорошо соответствует 8 insns, 11 uops (1.375) (считая vaddps как 2, но считая cmp + jl как один). Я нашел http://www.bnikolic.co.uk/blog/hpc-prof-events.html, в котором есть полный список поддерживаемых первичных событий, включая их коды для Sandybridge. (И инструкции о том, как выгрузить таблицу для любого другого процессора). (Ищите строку Code: в каждом блоке. Вам нужен байт umask, а затем код, как arg для perf.)

# a.out does only avg2, as an unrolled-by-4 version.
$ perf stat -d -e cycles,instructions,r14a1,r2b1,r10e,r2c2,r1c2 ./a.out
0.011331
1053298752.000000 1052674496.000000 1116959488.000000

 Performance counter stats for './a.out':

        42,250,312      cycles                    [34.11%]
        56,103,429      instructions              #    1.33  insns per cycle
        20,864,416      r14a1 # UOPS_DISPATCHED_PORT: 0x14=port2&3 loads
       111,943,380      r2b1 # UOPS_DISPATCHED: (2->umask 00 -> this core, any thread).
        72,208,772      r10e # UOPS_ISSUED: fused-domain
        71,422,907      r2c2 # UOPS_RETIRED: retirement slots used (fused-domain)
       111,597,049      r1c2 # UOPS_RETIRED: ALL (unfused-domain)
                 0      L1-dcache-loads
            18,470      L1-dcache-load-misses     #    0.00% of all L1-dcache hits
             5,717      LLC-loads                                                    [66.05%]
                 0      LLC-load-misses:HG        #    0.00% of all LL-cache hits

       0.011920301 seconds time elapsed

Так что да, похоже, что это может считать fused-domain и unused-domain uops!

Unrolling by 8 не помогает вообще: все еще 42M циклов. (но до 61M insns и 97M uops, благодаря меньшему количеству накладных расходов на цикл). Аккуратно, clang использует sub $-128, %rsi вместо добавления, потому что -128 вписывается в imm8, но +128 этого не делает. Поэтому я предполагаю, что разворачивание на 4 достаточно, чтобы насытить порт добавления FP.

Что касается ваших 1avg-функций, которые возвращают один float, а не вектор, well clang не авто-векторизует первый, но gcc делает. Он испускает гигантский пролог и эпилог для выполнения скалярных сумм, пока не достигнет выровненного адреса, а затем 32B AVX vaddps в небольшом цикле. Вы говорите, что нашли гораздо большую скорость с ними, но могли ли вы тестировать с меньшим буфером? Это означало бы появление большого ускорения для векторного кода и не-вектора.