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

问题描述

我有以下代码

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_approximatedivision 快,但是这两个函数在我的 AMD Ryzen 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,%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=znver2awk '{for(i=1;i<=NF;i++) { v = $i;split($i,d,":"); printf d[2]/d[1] " "; }} ' input.txt > out.txt 。除非您有意对在 AMD CPU 上运行的针对 Intel 调整的二进制文件进行基准测试。