Инструкция FMA, показанная как три упакованных двойных операции?

Я анализирую фрагмент кода линейной алгебры, который напрямую вызывает встроенную функцию, например

v_dot0  = _mm256_fmadd_pd( v_x0, v_y0, v_dot0 );

Мой тестовый скрипт вычисляет скалярное произведение двух векторов двойной точности длины 4 (поэтому только один вызов _mm256_fmadd_pd необходимо), повторяется 1 миллиард раз. Когда я считаю количество операций с perf Я получаю что-то следующее:

Performance counter stats for './main':

             0      r5380c7 (skl::FP_ARITH:512B_PACKED_SINGLE)                                                      (49.99%)
             0      r5340c7 (skl::FP_ARITH:512B_PACKED_DOUBLE)                                                      (49.99%)
             0      r5320c7 (skl::FP_ARITH:256B_PACKED_SINGLE)                                                      (49.99%)
 2'998'943'659      r5310c7 (skl::FP_ARITH:256B_PACKED_DOUBLE)                                                      (50.01%)
             0      r5308c7 (skl::FP_ARITH:128B_PACKED_SINGLE)                                                      (50.01%)
 1'999'928'140      r5304c7 (skl::FP_ARITH:128B_PACKED_DOUBLE)                                                      (50.01%)
             0      r5302c7 (skl::FP_ARITH:SCALAR_SINGLE)                                                           (50.01%)
 1'000'352'249      r5301c7 (skl::FP_ARITH:SCALAR_DOUBLE)                                                           (49.99%)

Я был удивлен, что число 256B_PACKED_DOUBLE операции ок. 3 миллиарда вместо 1 миллиарда, поскольку это инструкция из набора команд моей архитектуры. Почему perf рассчитывать 3 упакованные двойные операции за вызов _mm256_fmadd_pd ?

Примечание: чтобы проверить, что код не вызывает другие операции с плавающей запятой случайно, я закомментировал вызов вышеупомянутой внутренней функции, и perf считает ровно ноль 256B_PACKED_DOUBLE операции, как и ожидалось.

Изменить: MCVE, как и было запрошено:

ddot.c

#include <immintrin.h>  // AVX

double ddot(int m, double *x, double *y) {
    int ii;
    double dot = 0.0;

    __m128d u_dot0, u_x0, u_y0, u_tmp;
    __m256d v_dot0, v_dot1, v_x0, v_x1, v_y0, v_y1, v_tmp;

    v_dot0 = _mm256_setzero_pd();
    v_dot1 = _mm256_setzero_pd();
    u_dot0 = _mm_setzero_pd();

    ii = 0;

    for (; ii < m - 3; ii += 4) {
        v_x0 = _mm256_loadu_pd(&x[ii + 0]);
        v_y0 = _mm256_loadu_pd(&y[ii + 0]);
        v_dot0 = _mm256_fmadd_pd(v_x0, v_y0, v_dot0);
    }
    // reduce
    v_dot0 = _mm256_add_pd(v_dot0, v_dot1);
    u_tmp = _mm_add_pd(_mm256_castpd256_pd128(v_dot0), _mm256_extractf128_pd(v_dot0, 0x1));
    u_tmp = _mm_hadd_pd(u_tmp, u_tmp);
    u_dot0 = _mm_add_sd(u_dot0, u_tmp);
    _mm_store_sd(&dot, u_dot0);
    return dot;
}

main.c:

#include <stdio.h>

double ddot(int, double *, double *);

int main(int argc, char const *argv[]) {
    double x[4] = {1.0, 2.0, 3.0, 4.0}, y[4] = {5.0, 5.0, 5.0, 5.0};
    double xTy;
    for (int i = 0; i < 1000000000; ++i) {
        ddot(4, x, y);
    }
    printf(" %f\n", xTy);
    return 0;
}

я бегу perf как

sudo perf stat -e r5380c7 -e r5340c7 -e r5320c7 -e r5310c7 -e r5308c7 -e r5304c7 -e r5302c7 -e r5301c7 ./a.out

Разборка ddot выглядит следующим образом:

0000000000000790 <ddot>:
 790:   83 ff 03                cmp    $0x3,%edi
 793:   7e 6b                   jle    800 <ddot+0x70>
 795:   8d 4f fc                lea    -0x4(%rdi),%ecx
 798:   c5 e9 57 d2             vxorpd %xmm2,%xmm2,%xmm2
 79c:   31 c0                   xor    %eax,%eax
 79e:   c1 e9 02                shr    $0x2,%ecx
 7a1:   48 83 c1 01             add    $0x1,%rcx
 7a5:   48 c1 e1 05             shl    $0x5,%rcx
 7a9:   0f 1f 80 00 00 00 00    nopl   0x0(%rax)
 7b0:   c5 f9 10 0c 06          vmovupd (%rsi,%rax,1),%xmm1
 7b5:   c5 f9 10 04 02          vmovupd (%rdx,%rax,1),%xmm0
 7ba:   c4 e3 75 18 4c 06 10    vinsertf128 $0x1,0x10(%rsi,%rax,1),%ymm1,%ymm1
 7c1:   01 
 7c2:   c4 e3 7d 18 44 02 10    vinsertf128 $0x1,0x10(%rdx,%rax,1),%ymm0,%ymm0
 7c9:   01 
 7ca:   48 83 c0 20             add    $0x20,%rax
 7ce:   48 39 c1                cmp    %rax,%rcx
 7d1:   c4 e2 f5 b8 d0          vfmadd231pd %ymm0,%ymm1,%ymm2
 7d6:   75 d8                   jne    7b0 <ddot+0x20>
 7d8:   c5 f9 57 c0             vxorpd %xmm0,%xmm0,%xmm0
 7dc:   c5 ed 58 d0             vaddpd %ymm0,%ymm2,%ymm2
 7e0:   c4 e3 7d 19 d0 01       vextractf128 $0x1,%ymm2,%xmm0
 7e6:   c5 f9 58 d2             vaddpd %xmm2,%xmm0,%xmm2
 7ea:   c5 f9 57 c0             vxorpd %xmm0,%xmm0,%xmm0
 7ee:   c5 e9 7c d2             vhaddpd %xmm2,%xmm2,%xmm2
 7f2:   c5 fb 58 d2             vaddsd %xmm2,%xmm0,%xmm2
 7f6:   c5 f9 28 c2             vmovapd %xmm2,%xmm0
 7fa:   c5 f8 77                vzeroupper 
 7fd:   c3                      retq   
 7fe:   66 90                   xchg   %ax,%ax
 800:   c5 e9 57 d2             vxorpd %xmm2,%xmm2,%xmm2
 804:   eb da                   jmp    7e0 <ddot+0x50>
 806:   66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
 80d:   00 00 00 

1 ответ

Решение

Я только что проверил с помощью цикла asm на SKL. Инструкции FMA как vfmadd231pd ymm0, ymm1, ymm3 рассчитывает на 2 счета fp_arith_inst_retired.256b_packed_double даже если это один моп!

Я думаю, что Intel действительно хотел счетчик FLOP, а не счетчик команд или UOP.

Ваш третий 256-битный FP FP, вероятно, происходит от чего-то еще, что вы делаете, например, от горизонтальной суммы, которая начинает делать 256-битное перемешивание и еще одно 256-битное добавление, вместо того, чтобы сначала уменьшить до 128-битного. Я надеюсь, что вы не используете _mm256_hadd_pd!


Внутренний цикл тестового кода:

$ asm-link -d -n "testloop.asm"  # assemble with NASM -felf64 and link with ld into a static binary

    mov     ebp, 100000000    # setup stuff outside the loop
    vzeroupper

0000000000401040 <_start.loop>:
  401040:       c4 e2 f5 b8 c3          vfmadd231pd ymm0,ymm1,ymm3
  401045:       c4 e2 f5 b8 e3          vfmadd231pd ymm4,ymm1,ymm3
  40104a:       ff cd                   dec    ebp
  40104c:       75 f2                   jne    401040 <_start.loop>


$ taskset -c 3 perf stat -etask-clock,context-switches,cpu-migrations,page-faults,cycles,branches,instructions,uops_issued.any,uops_executed.thread,fp_arith_inst_retired.256b_packed_double -r4 ./"$t"


 Performance counter stats for './testloop-cvtss2sd' (4 runs):

            102.67 msec task-clock                #    0.999 CPUs utilized            ( +-  0.00% )
                 2      context-switches          #   24.510 M/sec                    ( +- 20.00% )
                 0      cpu-migrations            #    0.000 K/sec                  
                 2      page-faults               #   22.059 M/sec                    ( +- 11.11% )
       400,388,898      cycles                    # 3925381.355 GHz                   ( +-  0.00% )
       100,050,708      branches                  # 980889291.667 M/sec               ( +-  0.00% )
       400,256,258      instructions              #    1.00  insn per cycle           ( +-  0.00% )
       300,377,737      uops_issued.any           # 2944879772.059 M/sec              ( +-  0.00% )
       300,389,230      uops_executed.thread      # 2944992450.980 M/sec              ( +-  0.00% )
       400,000,000      fp_arith_inst_retired.256b_packed_double # 3921568627.451 M/sec            

         0.1028042 +- 0.0000170 seconds time elapsed  ( +-  0.02% )

400M насчитывает fp_arith_inst_retired.256b_packed_double для инструкций 200M FMA / итераций цикла 100M.

(ИДК, что с perf 4.20.g8fe28c + ядро 4.20.3-arch1-1-ARCH, Они рассчитывают посекундные вещи с десятичной дробью в неправильном месте для единицы. например, 3925381,355 кГц является правильным, а не ГГц. Не уверен, что это ошибка в perf или в ядре.

Без vzeroupper я иногда вижу задержку 5 циклов, а не 4, для FMA. IDK, если ядро ​​вышло из реестра в загрязненном состоянии или что-то в этом роде.


Почему я получаю три, а не два? (см. MCVE, добавленный к оригинальному сообщению)

Ваш ddot4 работает _mm256_add_pd(v_dot0, v_dot1); в начале очистки, и так как вы вызываете ее с size=4, вы получаете очистку один раз для FMA.

Обратите внимание, что ваш v_dot1 всегда равен нулю (потому что вы на самом деле не развернули с двумя аккумуляторами, как вы планируете?) Так что это бессмысленно, но процессор этого не знает. Мое предположение было неверным, это не 256-битный хадд, это просто бесполезное 256-битное вертикальное добавление.

(Для больших векторов, да, несколько аккумуляторов очень ценны, чтобы скрыть задержку FMA. Вам понадобится как минимум 8 векторов. См. Почему mulss занимает всего 3 цикла в Haswell, в отличие от таблиц инструкций Агнера? Чтобы узнать больше о развертывании с несколькими аккумуляторами. Но тогда вам понадобится цикл очистки, который выполняет 1 вектор за один раз, пока не дойдете до последних до 3 элементов.)

Кроме того, я думаю, что ваш финал _mm_add_sd(u_dot0, u_tmp); на самом деле это ошибка: вы уже добавили последнюю пару элементов с неэффективным 128-битным хэдом, так что это удваивает самый низкий элемент.

См. Раздел Получение суммы значений, хранящихся в __m256d с помощью SSE/AVX, для способа, который не сосет.


Также обратите внимание, что GCC разделяет ваши невыровненные нагрузки на 128-битные половины vinsertf128 потому что вы скомпилировали по умолчанию -mtune=generic (который одобряет Sandybridge) вместо использования -march=haswell включить AVX+FMA и установить -mtune=haswell, (Или использовать -march=native)

Другие вопросы по тегам