为什么 clang 使 Quake 快速反平方根代码比使用 GCC 快 10 倍?(带有 *(long*)float 类型双关语)

包子9

我正在尝试对快速反平方根进行基准测试。完整代码在这里:

#include <benchmark/benchmark.h>
#include <math.h>

float number = 30942;
    
static void BM_FastInverseSqrRoot(benchmark::State &state) {
    for (auto _ : state) {
        // from wikipedia:
        long i;
        float x2, y;
        const float threehalfs = 1.5F;

        x2 = number * 0.5F;
        y  = number;
        i  = * ( long * ) &y;
        i  = 0x5f3759df - ( i >> 1 );
        y  = * ( float * ) &i;
        y  = y * ( threehalfs - ( x2 * y * y ) );
        //  y  = y * ( threehalfs - ( x2 * y * y ) );
        
        float result = y;
        benchmark::DoNotOptimize(result);
    }
}


static void BM_InverseSqrRoot(benchmark::State &state) {
    for (auto _ : state) {
        float result = 1 / sqrt(number);
        benchmark::DoNotOptimize(result);
    } 
}

BENCHMARK(BM_FastInverseSqrRoot);
BENCHMARK(BM_InverseSqrRoot);

如果您想自己运行这里是 quick-bench 中的代码。

使用 GCC 11.2 和 -O3 编译,BM_FastInverseSqrRoot 比 Noop 慢大约 31 倍(当我在我的机器上本地运行它时大约 10 ns)。使用 Clang 13.0 和 -O3 编译,它比 Noop 慢大约 3.6 倍(当我在我的机器上本地运行它时大约 1 ns)。这是 10 倍的速度差异。

这是相关的程序集(取自快速工作台)。

使用 GCC:

               push   %rbp
               mov    %rdi,%rbp
               push   %rbx
               sub    $0x18,%rsp
               cmpb   $0x0,0x1a(%rdi)
               je     408c98 <BM_FastInverseSqrRoot(benchmark::State&)+0x28>
               callq  40a770 <benchmark::State::StartKeepRunning()>
  408c84       add    $0x18,%rsp
               mov    %rbp,%rdi
               pop    %rbx
               pop    %rbp
               jmpq   40aa20 <benchmark::State::FinishKeepRunning()>
               nopw   0x0(%rax,%rax,1)
  408c98       mov    0x10(%rdi),%rbx
               callq  40a770 <benchmark::State::StartKeepRunning()>
               test   %rbx,%rbx
               je     408c84 <BM_FastInverseSqrRoot(benchmark::State&)+0x14>
               movss  0x1b386(%rip),%xmm4        # 424034 <_IO_stdin_used+0x34>
               movss  0x1b382(%rip),%xmm3        # 424038 <_IO_stdin_used+0x38>
               mov    $0x5f3759df,%edx
               nopl   0x0(%rax,%rax,1)
   408cc0      movss  0x237a8(%rip),%xmm0        # 42c470 <number>
               mov    %edx,%ecx
               movaps %xmm3,%xmm1
        2.91%  movss  %xmm0,0xc(%rsp)
               mulss  %xmm4,%xmm0
               mov    0xc(%rsp),%rax
        44.70% sar    %rax
        3.27%  sub    %eax,%ecx
        3.24%  movd   %ecx,%xmm2
        3.27%  mulss  %xmm2,%xmm0
        9.58%  mulss  %xmm2,%xmm0
        10.00% subss  %xmm0,%xmm1
        10.03% mulss  %xmm2,%xmm1
        9.64%  movss  %xmm1,0x8(%rsp)
        3.33%  sub    $0x1,%rbx
               jne    408cc0 <BM_FastInverseSqrRoot(benchmark::State&)+0x50>
               add    $0x18,%rsp
               mov    %rbp,%rdi
               pop    %rbx
               pop    %rbp
  408d0a       jmpq   40aa20 <benchmark::State::FinishKeepRunning()>

使用铿锵声:

           push   %rbp
           push   %r14
           push   %rbx
           sub    $0x10,%rsp
           mov    %rdi,%r14
           mov    0x1a(%rdi),%bpl
           mov    0x10(%rdi),%rbx
           call   213a80 <benchmark::State::StartKeepRunning()>
           test   %bpl,%bpl
           jne    212e69 <BM_FastInverseSqrRoot(benchmark::State&)+0x79>
           test   %rbx,%rbx
           je     212e69 <BM_FastInverseSqrRoot(benchmark::State&)+0x79>
           movss  -0xf12e(%rip),%xmm0        # 203cec <_IO_stdin_used+0x8>
           movss  -0xf13a(%rip),%xmm1        # 203ce8 <_IO_stdin_used+0x4>
           cs nopw 0x0(%rax,%rax,1)
           nopl   0x0(%rax)
 212e30 2.46%  movd   0x3c308(%rip),%xmm2        # 24f140 <number>
        4.83%  movd   %xmm2,%eax
        8.07%  mulss  %xmm0,%xmm2
        12.35% shr    %eax
        2.60%  mov    $0x5f3759df,%ecx
        5.15%  sub    %eax,%ecx
        8.02%  movd   %ecx,%xmm3
        11.53% mulss  %xmm3,%xmm2
        3.16%  mulss  %xmm3,%xmm2
        5.71%  addss  %xmm1,%xmm2
        8.19%  mulss  %xmm3,%xmm2
        16.44% movss  %xmm2,0xc(%rsp)
        11.50% add    $0xffffffffffffffff,%rbx
               jne    212e30 <BM_FastInverseSqrRoot(benchmark::State&)+0x40>
 212e69        mov    %r14,%rdi
               call   213af0 <benchmark::State::FinishKeepRunning()>
               add    $0x10,%rsp
               pop    %rbx
               pop    %r14
               pop    %rbp
  212e79       ret

他们看起来和我很相似。两者似乎都在使用 SIMD 寄存器/指令,例如mulss. GCC 版本有一个sar据说占 46% 的?(但我认为它只是贴错了标签,而且它mulss, mov, sar加起来占 46%)。无论如何,我对 Assembly 还不够熟悉,无法真正说出造成如此巨大性能差异的原因。

有人知道吗?

彼得·科德斯

仅供参考,现在还值得在 ​​x86-64 上使用 Quake 快速逆平方根算法吗?- 不,已被 SSE1 淘汰rsqrtss,您可以使用或不使用牛顿迭代。

正如人们在评论中指出的那样,您使用的是 64 位long(因为这是非 Windows 系统上的 x86-64),将其指向 32 位float. 因此,除了严格的混叠违规(使用memcpystd::bit_cast<int32_t>(myfloat)用于类型双关语),这也是性能和正确性的阻碍。

您的perf report输出证实了这一点;GCC 正在对堆栈进行 32 位movss %xmm0,0xc(%rsp)存储,然后是 64 位 reload mov 0xc(%rsp),%rax,这将导致存储转发停止,从而导致额外的延迟。还有吞吐量损失,因为实际上您是在测试吞吐量,而不是延迟:逆 sqrt 的下一次计算只有一个恒定输入,而不是前一次迭代的结果。benchmark::DoNotOptimize包含一个"memory"clobber,它阻止GCC/clang将大部分计算提升到循环之外;他们必须假设number可能已经改变,因为它不是const。)

sar像往常一样,等待加载结果的指令 (the ) 是这些周期的罪魁祸首。(当一个中断触发以在事件计数器回绕时收集样本时cycles,CPU 必须找出一条指令应归咎于该事件。通常这最终是等待较早的慢指令的一条,或者可能只是在一个即使没有数据依赖性,指令也很慢,我忘记了。)

Clang 选择假设高 32 位为零,因此movd %xmm0, %eax仅使用 ALU uop 复制寄存器,而shr不是因为它知道它正在从它假装使用sar的 64 位的高半部分移入零。long(仍然使用一个函数调用%rdi,所以这不是 Windows 叮当声。)


修正版本:GCC 和 clang 做类似的 asm

修复问题中快速工作台链接上的代码以使用int32_tstd::bit_casthttps: //godbolt.org/z/qbxqsaW4e 显示 GCC 和 clang 编译与 类似-Ofast,尽管不相同。例如,GCC 加载number两次,一次加载到整数寄存器,一次加载到 XMM0。Clang 加载一次并用于movd eax, xmm2获取它。

在 QB(https://quick-bench.com/q/jYLeX2krrTs0afjQKFp6Nm_G2v8)上,现在 GCC 的 BM_FastInverseSqrRoot 比原始版本快 2 倍,没有 -ffast-math

是的sqrtss由于C++. 它确实每次都检查数字,因为 quick-bench 不允许编译以省略该检查以调用 libm 函数。但是该分支预测完美,因此循环应该仍然很容易成为端口 0 吞吐量(div/sqrt 单位)的瓶颈。divss-ffast-mathsqrtfsqrt(float)>=0-fno-math-errno

Quick-bench 确实允许-Ofast,这相当于-O3 -ffast-math,它使用rsqrtss和 牛顿迭代。(使用 FMA 会更快,但 quick-bench 不允许-march=native或任何东西。我想可以使用__attribute__((target("avx,fma"))).

Quick-bench 现在通过权限错误映射页面Error or timeout给出我是否使用它。并建议一个更小的,所以我不能在那个系统上测试。-m/--mmap_pages

带有牛顿迭代的 rsqrt(就像编译器使用 at 一样-Ofast)可能更快或类似于 Quake 的快速 invsqrt,但精度约为 23 位。

本文收集自互联网,转载请注明来源。

如有侵权,请联系 [email protected] 删除。

编辑于
0

我来说两句

0 条评论
登录 后参与评论

相关文章