FMA 指令显示为三个压缩双操作?

Posted

技术标签:

【中文标题】FMA 指令显示为三个压缩双操作?【英文标题】:FMA instruction showing up as three packed double operations? 【发布时间】:2019-01-29 10:54:18 【问题描述】:

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

v_dot0  = _mm256_fmadd_pd( v_x0, v_y0, v_dot0 );

我的测试脚本计算两个长度为 4 的双精度向量的点积(因此只需要一次调用 _mm256_fmadd_pd),重复 10 亿次。当我用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 操作,正如预期的那样。

编辑: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 

【问题讨论】:

发布您的代码和编译器选项的minimal reproducible example,或者为您实际测量的循环提供更好的asm,这样我们就可以查看是否真的只有一个...pd ymm 数学指令。你最后没有做一个水平求和,是你,实际上得到标量结果吗?或者也许你是,那是 128 和标量。你是用_mm256_hadd_pd还是什么的? 我添加了 MCVE。我做了一个水平总和,但不是256 一个。 您的 ddot4 在清理开始时运行 _mm256_add_pd(v_dot0, v_dot1);,由于您使用 size=4 调用它,因此每个 FMA 都会进行一次清理。请注意,您的 v_dot1 始终为零,因此这是没有意义的,但 CPU 不知道这一点。我的猜测是错误的,它不是 256 位的 hadd,它只是一个无用的 256 位垂直添加。我认为最后的 _mm_add_sd(u_dot0, u_tmp); 实际上是一个错误:您已经添加了最后一对元素,其效率低下 128 位 hadd,所以这会重复计算最低元素。 请参阅Get sum of values stored in __m256d with SSE/AVX 以了解不糟糕的方法。 @PeterCordes:请善待。谢谢你的评论,我错过了额外的add。你是对的,实际上有用于展开 8 个向量的代码,但我为了最小的例子删除了它。请注意,这不是我的代码,我只是想理解别人写的代码。 【参考方案1】:

我刚刚在 SKL 上使用 asm 循环进行了测试。像 vfmadd231pd ymm0, ymm1, ymm3 这样的 FMA 指令算作 fp_arith_inst_retired.256b_packed_double 的 2 次计数,即使它是一个微指令!

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

您的第三个 256 位 FP uop 可能来自您正在执行的其他操作,例如开始执行 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 循环迭代。

(IDK perf 4.20.g8fe28c + kernel 4.20.3-arch1-1-ARCH 怎么回事。他们计算每秒的内容,小数点在单位的错误位置。例如,3925381.355 kHz 是正确的,而不是 GHz。不确定是否是perf 或内核中的错误。

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


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

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

请注意,您的 v_dot1 始终为零(因为您实际上并没有像您计划的那样使用 2 个累加器展开?)所以这是没有意义的,但 CPU 不知道这一点。我的猜测是错误的,它不是 256 位的 hadd,它只是一个无用的 256 位垂直添加。

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

另外,我认为你最后的 _mm_add_sd(u_dot0, u_tmp); 实际上是一个错误:你已经添加了最后一对元素,其效率低下 128 位 hadd,所以这会重复计算最低元素。

请参阅Get sum of values stored in __m256d with SSE/AVX 了解不糟糕的方法。


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

【讨论】:

感谢您的检查。所以我们在同一页面上,一个256b_packed_double 不映射到一个vfmadd231pd。为什么我得到三个,而不是两个? (请参阅添加到原始帖子的 MCVE) 感谢有关编译器标志的提示。我确实添加了-mavx2-mfma,但显然这还不够。 @Nibor:启用这些指令集就足够了,但不能真正调整拥有它们的 CPU。 :P 请参阅 gcc.gnu.org/bugzilla/show_bug.cgi?id=80568 和 gcc.gnu.org/bugzilla/show_bug.cgi?id=78762 以获取让 -mavx2 为可以运行代码的 CPU 设置调整选项的功能请求,例如 tune=generic-avx2 如果存在这样的事情。 perf stat 费率错误已在 lore.kernel.org/patchwork/patch/1025968 中修复 @Zulan:谢谢,希望 Arch Linux 能尽快更新他们的 perf 包。有点失望的是,一个月大的超级明显的错误(对于任何使用perf stat 的人)在 Arch 回购中仍未修复:/

以上是关于FMA 指令显示为三个压缩双操作?的主要内容,如果未能解决你的问题,请参考以下文章

FMA指令集的硬件支持有多丰富

如何以编程方式检查 CPU 上是不是启用了 fused mul add (FMA) 指令?

如何让 CC 2.0 和 3.0 编译器生成 FMA 指令?

fma 中每个周期的指令数是多少,带有负数?

Linux系统Linux入门(中) {基本指令:输入输出,重定向输入输出,管道,显示时间和日期,打包和压缩,打包VS压缩,包和文件}

entos7指令操作