问题描述
void division_approximate(float a[],float b[],float c[],int n) {
// c[i] = a[i] * (1 / b[i]);
for (int i = 0; i < n; i+=8) {
__m256 b_val = _mm256_loadu_ps(b + i);
b_val = _mm256_rcp_ps(b_val);
__m256 a_val = _mm256_loadu_ps(a + i);
a_val = _mm256_mul_ps(a_val,b_val);
_mm256_storeu_ps(c + i,a_val);
}
}
void division(float a[],int n) {
// c[i] = a[i] / b[i];
for (int i = 0; i < n; i+=8) {
__m256 b_val = _mm256_loadu_ps(b + i);
__m256 a_val = _mm256_loadu_ps(a + i);
a_val = _mm256_div_ps(a_val,a_val);
}
}
我希望 division_approximate
比 division
快,但是这两个函数在我的 AMD Ryzen 7 4800H 上花费的时间几乎相同。我不明白为什么,我希望 division_approximate
明显更快。这个问题在 GCC 和 CLANG 上都会重现。使用 -O3 -march=core-avx2
编译。
更新
division
│ >0x555555555c38 <division+88> vmovups 0x0(%r13,%rax,4),%ymm3 │
│ 0x555555555c3f <division+95> vdivps (%r14,%ymm3,%ymm0 │
│ 0x555555555c45 <division+101> vmovups %ymm0,(%rbx,4) │
│ 0x555555555c4a <division+106> add $0x8,%rax │
│ 0x555555555c4e <division+110> cmp %eax,%r12d │
│ 0x555555555c51 <division+113> jg 0x555555555c38 <division+88> │
division_approximate
│ >0x555555555b38 <division_approximate+88> vrcpps (%r14,%ymm0 │
│ 0x555555555b3e <division_approximate+94> vmulps 0x0(%r13,%ymm0,%ymm0 │
│ 0x555555555b45 <division_approximate+101> vmovups %ymm0,4) │
│ 0x555555555b4a <division_approximate+106> add $0x8,%rax │
│ 0x555555555b4e <division_approximate+110> cmp %eax,%r12d │
│ 0x555555555b51 <division_approximate+113> jg 0x555555555b38 <division_approximate+88> │
对于 n = 256 * 1024 * 1024
,两个代码的执行时间几乎完全相同(318 毫秒与 319 毫秒)。
解决方法
* 4
/ 0.318 / 1000^2 * 4
(每个浮点的字节数)_mm256_stream_ps
大约是 13.5 GB/s 的 DRAM 带宽,或大约 10.1GB/s 的有用流带宽。 (假设商店实际上花费了 RFO 的读写带宽;正如 Jerome 指出的那样,(256 * 1024 * 1024 / 8) / 0.318 / 1000^3
可以使商店只花费一次,而不是两次。)
IDK 对于 Zen 2 上的单线程三元组带宽是好是坏,但这只是vdivps
= 每纳秒 ~0.1055 个向量(8 个浮点数); Zen 2 vdivps ymm
可以在 0.36 GHz 上跟上。我假设你的 CPU 比那个快:P
(0.1055 vec/ns * 3.5 周期/vec = 0.36 周期/ns 又名 GHz)
非常明显的内存瓶颈,远不及 Zen2 每 3.5 个周期 _mm256_stream_ps
吞吐量。 (CodeSandbox)。 使用更小的数组(适合 L1 或至少 L2 缓存)并循环多次。
尽量避免在实际代码中编写这样的循环。计算强度(每次将数据加载到 L1 缓存或寄存器时的工作量)非常低。作为另一遍的一部分执行此操作,或使用缓存阻塞为输入的一小部分执行此操作,然后在缓存中仍然很热时消耗该输出的一小部分。 (这比使用 vdivps
绕过缓存要好得多。)
当与其他操作(大量 fmas / mul / add)混合时,rcpps
通常是比 rcpps + 牛顿迭代更好的选择(这通常是可接受精度所必需的:原始 vdivps
是只有大约 11 位 IIRC。)vmovups
只是单个 uop,与单独的 rcpps 和 mulps uop 相比。 (尽管从内存中,vdivps 无论如何都需要单独的 -march=core-avx2
加载,并且 Zen 可以将内存源折叠为单个 uop 没有问题)。另请参阅https://uops.info/ re:前端吞吐量与划分单元瓶颈,如果您只是进行划分,而不是将其与其他操作混合。
当然,如果您可以完全避免除法,那就太好了,例如从循环中提升倒数并乘以,但现代 CPU 具有足够好的分频器硬件,即使您没有内存瓶颈,通常也不会从 rcpps 中获得太多收益。例如在使用两个多项式的比率评估多项式近似时,FMA 的数量通常足以隐藏 vdivps 的吞吐量成本,而牛顿迭代将花费更多 FMA uop。
此外,当您没有英特尔“核心”微架构时,为什么要-march=native
?使用 -march=znver2
或 awk '{for(i=1;i<=NF;i++) { v = $i;split($i,d,":"); printf d[2]/d[1] " "; }} ' input.txt > out.txt
。除非您有意对在 AMD CPU 上运行的针对 Intel 调整的二进制文件进行基准测试。