正则除法与近似倒数乘法一样快.为什么?



我有以下代码:

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[], float b[], float c[], 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, b_val);
_mm256_storeu_ps(c + i, a_val);
}
}

我希望division_approximatedivision更快,但这两个功能在我的 AMD 锐龙 7 4800H 上花费的时间几乎相同。我不明白为什么,我希望division_approximate明显更快。此问题在 GCC 和 CLANG 上重现。用-O3 -march=core-avx2编译。

更新

以下是GCC 9.3为这两个循环生成的源代码:

division
│  >0x555555555c38 <division+88>    vmovups 0x0(%r13,%rax,4),%ymm3                                                                                                                                                                                                                       │
│   0x555555555c3f <division+95>    vdivps (%r14,%rax,4),%ymm3,%ymm0                                                                                                                                                                                                                     │
│   0x555555555c45 <division+101>   vmovups %ymm0,(%rbx,%rax,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,%rax,4),%ymm0                                                                                                                                                                                                           │
│   0x555555555b3e <division_approximate+94>        vmulps 0x0(%r13,%rax,4),%ymm0,%ymm0                                                                                                                                                                                                  │
│   0x555555555b45 <division_approximate+101>       vmovups %ymm0,(%rbx,%rax,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 毫秒)。

(256 * 1024 * 1024)* 4(每个浮点数的字节数)/ 0.318 / 1000^2 * 4约为 13.5 GB/s DRAM 带宽,或大约 10.1GB/s 的有用流带宽。 (假设商店实际上需要为RFO花费读+写带宽;正如Jerome指出的那样,_mm256_stream_ps可能会使商店只花费一次,而不是两次。

IDK,如果这对Zen 2上的单线程三元组带宽是好是坏,但这只是
(256 * 1024 * 1024 / 8) / 0.318 / 1000^3= ~0.1055个向量(8个浮点数)每纳秒; Zen 2vdivps可以在0.36 GHz时跟上。我假设您的 CPU 比这更快 :P
(0.1055 vec/ns * 3.5 周期/vec = 0.36 周期/ns(又称 GHz)

所以很明显是内存瓶颈,远不及 Zen2 每 3.5 个周期vdivps ymm吞吐量的一个。(https://uops.info/)。使用更小的阵列(适合 L1 或至少 L2 缓存)并多次遍历它。


尽量避免在实际代码中编写这样的循环。计算强度(每次将数据加载到L1缓存或寄存器时的工作)非常低。 在另一次传递中执行此操作,或使用缓存阻塞对一小部分输入执行此操作,然后在缓存中仍处于热状态时消耗该一小部分输出。(这比使用_mm256_stream_ps绕过缓存要好得多。

当与其他操作(大量 fmas/mul/add )混合时,vdivps通常是比 rcpps + 牛顿迭代更好的选择(通常需要可接受的精度:原始rcpps只有大约 11 位 IIRC)。vdivps只是单个 UOP,而不是单独的 RCPP 和 mulps uop。 (尽管从内存中,无论如何使用 vdivps 都需要单独的vmovups加载,并且 Zen 将内存源折叠成单个 uop 没有问题)。另请参阅浮点除法与浮点乘法 re:前端吞吐量与除法单位瓶颈,如果您只是除法,而不是将其与其他操作混合。

当然,如果你能完全避免除法,例如将倒数从循环中提升出来并乘法,那就太好了,但现代 CPU 具有足够好的除法器硬件,即使你没有内存瓶颈,通常也不会从 rcpps 中获得太多收益。 例如,在使用两个多项式的比值评估多项式近似时, FMA 的数量通常足以隐藏 vdivps 的吞吐量成本,而牛顿迭代将花费更多的 FMA uops。


另外,当您没有英特尔"核心"微架构时,为什么要-march=core-avx2? 使用-march=native-march=znver2。 除非您有意对针对在 AMD CPU 上运行的英特尔优化的二进制文件进行基准测试。

最新更新