为什么 u64::trailing_zeros() 在无分支工作时生成分支程序集?

6 optimization x86-64 llvm rust

这个功能:

pub fn g(n: u64) -> u32 {
    n.trailing_zeros()
}
Run Code Online (Sandbox Code Playgroud)

生成带有分支的程序集:

playground::g:
    testq   %rdi, %rdi
    je  .LBB0_1
    bsfq    %rdi, %rax
    retq

.LBB0_1:
    movl    $64, %eax
    retq
Run Code Online (Sandbox Code Playgroud)

这个替代功能

playground::g:
    testq   %rdi, %rdi
    je  .LBB0_1
    bsfq    %rdi, %rax
    retq

.LBB0_1:
    movl    $64, %eax
    retq
Run Code Online (Sandbox Code Playgroud)

生成没有分支的程序集:

playground::g:
    bsfq    %rdi, %rcx
    xorl    %eax, %eax
    cmpq    $1, %rdi
    sbbl    %eax, %eax
    orl %ecx, %eax
    retq
Run Code Online (Sandbox Code Playgroud)

事实证明,仅当返回的常量为 64 时才会创建分支。返回0、 或u32::MAX或任何其他数字会生成无分支程序集。

为什么是这样?只是优化器的一个怪癖还是有原因?

我正在尝试创建高性能、无分支的代码。

使用 Rust 1.65 发布配置文件

Pit*_*taJ 5

trailing_zeros对应于cttzLLVM 内在.

该内在函数恰好在 x86-64 上编译为以下指令:

g:                                      # @g
        test    rdi, rdi
        je      .LBB0_1
        bsf     rax, rdi
        ret
.LBB0_1:
        mov     eax, 64
        ret
Run Code Online (Sandbox Code Playgroud)

当输入值为 0 时,该内在函数的输出是整数的位宽。LLVM 能够识别冗余操作并将其删除,这就是为什么u64::BITSor 只是64在您的条件结果中产生与内在函数相同的机器代码。

看来使用任何其他数字都会导致编译器将内部分支识别为死代码,因此将其删除:

e:                                      # @e
        xor     ecx, ecx
        bsf     rax, rdi
        cmove   eax, ecx
        ret
Run Code Online (Sandbox Code Playgroud)

相反,会生成单个条件移动。我相信,当涉及某些内在函数时,输出的这种差异只是 LLVM x86-64 汇编器的一个怪癖。

您可以使用 clang 重现与 C 相同的差异。神箭

为此可能值得提出 LLVM 问题,但前提是无分支版本实际上更好。

这个 LLVM 问题可能相关

  • 是的,这是有道理的,即使更小的代码大小也能带来好处。事实上,LLVM 在其他情况下使用“cmove”,这使得这看起来可能更具可选性。但如果可能的话,我建议尝试实际进行基准测试。 (3认同)