FMA指令显示为三个打包双操作吗?

时间:2019-01-29 10:54:19

标签: linear-algebra intrinsics perf

我正在分析一段线性代数代码,例如calling intrinsics directly

v_dot0  = _mm256_fmadd_pd( v_x0, v_y0, v_dot0 );

我的测试脚本计算了两个长度为4的双精度向量的点积(因此只需要一次调用_mm256_fmadd_pd),重复十亿次。当我用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操作的数量大约为。 30亿,而不是10亿,因为这是我的体系结构指令集中的一条指令。 为什么perf每次调用_mm256_fmadd_pd时都要进行3次压缩双重操作?

注意:为了测试代码是否没有意外调用其他浮点运算,我注释了对上述内在函数的调用,并且按预期,perf计数为零个256B_PACKED_DOUBLE运算。< / p>

根据要求编辑: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 个答案:

答案 0 :(得分:2)

我刚刚在SKL上使用asm循环进行了测试。像fp_arith_inst_retired.256b_packed_double这样的FMA指令算上_mm256_hadd_pd的2个计数,即使它是单个uop!

我猜英特尔真的想要FLOP计数器,而不是指令或uop计数器。

您的第3个256位FP uop可能来自您正在执行的其他操作,例如,水平和开始时先进行256位混洗,然后再添加256位,而不是先减少到128位。希望您不要使用$ 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% )


测试代码内循环:

fp_arith_inst_retired.256b_packed_double

对于200M FMA指令/ 100M循环迭代,perf的计数为400M。

(IDK用4.20.3-arch1-1-ARCH处理4.20.g8fe28c +内核ddot4。它们计算每秒的内容,用十进制小数表示单位错误的位置。例如3925381.355 kHz是正确的,而不是GHz。不确定是perf还是内核中的错误。

没有vzeroupper,对于FMA,我有时会看到5个周期的延迟,而不是4个周期。如果内核将寄存器留在污染状态或其他状态,则为IDK。


  

为什么我得到三个,而不是两个? (请参阅添加到原始帖子中的MCVE)

您的_mm256_add_pd(v_dot0, v_dot1);在清理开始时运行v_dot1 ,由于您使用size = 4进行调用,因此每个FMA都会进行一次清理。

请注意,您的_mm_add_sd(u_dot0, u_tmp);始终为零(因为您实际上并未像您计划的那样使用2个累加器展开?)这毫无意义,但是CPU并不知道。我的猜测是错误的,它不是256位的hadd,而是无用的256位垂直添加。

对于更大的向量,是许多累加器非常对于隐藏FMA延迟非常有价值。您至少需要8个向量。有关更多信息,请参见Why does mulss take only 3 cycles on Haswell, different from Agner's instruction tables?关于使用多个累加器进行展开。但是,您将需要一次清理循环,一次执行1个向量,直到最后3个元素为止。)

此外,我认为您最后的vinsertf128实际上是一个错误:您已经添加了最后一对具有低效128位hadd的元素,因此这将最低元素加倍了。

请参见Get sum of values stored in __m256d with SSE/AVX,以了解不会出现的问题。


还请注意,由于您使用默认的-mtune=generic(有利于Sandybridge)进行编译,而不是使用-march=haswell来启用AVX,因此GCC使用-mtune=haswell将未对齐的负载分成了128位的一半+ FMA并设置-march=native。 (或使用{{1}})