FMA 指令显示为三个打包的双重操作?
FMA instruction showing up as three packed double operations?
我正在分析一段线性代数代码,它是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 [=15=]x3,%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 [=15=]x2,%ecx
7a1: 48 83 c1 01 add [=15=]x1,%rcx
7a5: 48 c1 e1 05 shl [=15=]x5,%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 [=15=]x1,0x10(%rsi,%rax,1),%ymm1,%ymm1
7c1: 01
7c2: c4 e3 7d 18 44 02 10 vinsertf128 [=15=]x1,0x10(%rdx,%rax,1),%ymm0,%ymm0
7c9: 01
7ca: 48 83 c0 20 add [=15=]x20,%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 [=15=]x1,%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 指令计为 2 个 fp_arith_inst_retired.256b_packed_double
,即使它是一个 uop!
我猜英特尔真的想要一个 FLOP 计数器,而不是指令或 uop 计数器。
您的第 3 个 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 what up with perf
4.20.g8fe28c + kernel 4.20.3-arch1-1-ARCH
。他们计算每秒的东西,小数点在单位的错误位置。例如 3925381.355 kHz 是正确的,不是GHz。不确定是 perf 还是内核中的错误。
如果没有 vzeroupper,我有时会看到 FMA 的延迟为 5 个周期,而不是 4 个。 IDK 如果内核使寄存器处于污染状态或其他状态。
Why do I get three though, and not two? (see MCVE added to original post)
您的 ddot4
在清理开始时运行 _mm256_add_pd(v_dot0, v_dot1);
,并且由于您使用 size=4 调用它,所以每次清理一次FMA.
请注意,您的 v_dot1
始终为零(因为您实际上并没有像您计划的那样用 2 个累加器展开?)所以这是没有意义的,但是 CPU 没有我知道。我猜错了,它不是256位的hadd,它只是一个无用的256位垂直加。
(对于较大的向量,是的,多个累加器对于隐藏 FMA 延迟非常有价值。您至少需要 8 个向量。有关使用多个累加器展开的更多信息,请参阅 。但是您需要一个一次执行 1 个向量的清理循环,直到您处理到最后的最多 3 个元素。)
另外,我认为你最后的 _mm_add_sd(u_dot0, u_tmp);
实际上是一个错误:你已经添加了最后一对具有低效 128 位 hadd 的元素,所以这会重复计算最低的元素。
请参阅 以获取不糟透的方法。
另请注意,GCC 使用 vinsertf128
将未对齐的负载拆分为 128 位的一半,因为您使用默认的 -mtune=generic
(有利于 Sandybridge)进行编译,而不是使用 -march=haswell
来编译启用 AVX+FMA 并设置 -mtune=haswell
。 (或使用 -march=native
)
我正在分析一段线性代数代码,它是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 [=15=]x3,%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 [=15=]x2,%ecx
7a1: 48 83 c1 01 add [=15=]x1,%rcx
7a5: 48 c1 e1 05 shl [=15=]x5,%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 [=15=]x1,0x10(%rsi,%rax,1),%ymm1,%ymm1
7c1: 01
7c2: c4 e3 7d 18 44 02 10 vinsertf128 [=15=]x1,0x10(%rdx,%rax,1),%ymm0,%ymm0
7c9: 01
7ca: 48 83 c0 20 add [=15=]x20,%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 [=15=]x1,%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 指令计为 2 个 fp_arith_inst_retired.256b_packed_double
,即使它是一个 uop!
我猜英特尔真的想要一个 FLOP 计数器,而不是指令或 uop 计数器。
您的第 3 个 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 what up with perf
4.20.g8fe28c + kernel 4.20.3-arch1-1-ARCH
。他们计算每秒的东西,小数点在单位的错误位置。例如 3925381.355 kHz 是正确的,不是GHz。不确定是 perf 还是内核中的错误。
如果没有 vzeroupper,我有时会看到 FMA 的延迟为 5 个周期,而不是 4 个。 IDK 如果内核使寄存器处于污染状态或其他状态。
Why do I get three though, and not two? (see MCVE added to original post)
您的 ddot4
在清理开始时运行 _mm256_add_pd(v_dot0, v_dot1);
,并且由于您使用 size=4 调用它,所以每次清理一次FMA.
请注意,您的 v_dot1
始终为零(因为您实际上并没有像您计划的那样用 2 个累加器展开?)所以这是没有意义的,但是 CPU 没有我知道。我猜错了,它不是256位的hadd,它只是一个无用的256位垂直加。
(对于较大的向量,是的,多个累加器对于隐藏 FMA 延迟非常有价值。您至少需要 8 个向量。有关使用多个累加器展开的更多信息,请参阅
另外,我认为你最后的 _mm_add_sd(u_dot0, u_tmp);
实际上是一个错误:你已经添加了最后一对具有低效 128 位 hadd 的元素,所以这会重复计算最低的元素。
请参阅
另请注意,GCC 使用 vinsertf128
将未对齐的负载拆分为 128 位的一半,因为您使用默认的 -mtune=generic
(有利于 Sandybridge)进行编译,而不是使用 -march=haswell
来编译启用 AVX+FMA 并设置 -mtune=haswell
。 (或使用 -march=native
)