FMA指令显示为三个打包的双重操作



我正在分析一段直接调用内部函数的线性代数代码,例如

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个压缩双操作

注意:为了测试代码没有意外调用其他浮点运算,我注释掉了对上述内部函数的调用,perf256B_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(" %fn", 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 

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

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

你的第三个256位FP-up可能来自你正在做的其他事情,比如一个水平和,它开始做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% )

200M FMA指令/100M循环迭代的fp_arith_inst_retired.256b_packed_double计数为400M

(IDKperf4.20.g8fe28c+内核4.20.3-arch1-1-ARCH怎么了。他们计算每秒的数据时,小数点放错了单位的位置。例如3925381.355 kHz是正确的,而不是GHz。不确定是性能错误还是内核错误。

如果没有vzerooper,我有时会看到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个向量。请参阅为什么mulss在Haswell上只需要3个周期,与Agner';s的指令表不同?了解有关使用多个累加器展开的更多信息。但是,您需要一个清理循环,每次执行1个向量,直到最后一个up-to-3元素。)

此外,我认为您的最后一个_mm_add_sd(u_dot0, u_tmp);实际上是一个错误:您已经使用低效的128位hadd添加了最后一对元素,所以这会加倍计算最低的元素。

请参阅使用SSE/AVX获取存储在__m256d中的值的总和,了解一种不会出错的方法。


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

相关内容

最新更新