只有 1 种情况下 __builtin_clz 给出了错误的答案。我很好奇是什么导致了这种行为。

当我使用文字值 0 时,我总是按预期得到 32。但是 0 作为变量产生 31。为什么存储值 0 的方法很重要?

我参加了架构类(class),但不了解不同的程序集。看起来当给定文字值 0 时,即使没有优化,程序集也会以某种方式始终具有 32 硬编码的正确答案。并且在使用 -march=native 时计算前导零的方法是不同的。

This post 关于用 __builtin_clz 模拟 _BitScanReversebsrl %eax, %eax 行似乎暗示位扫描反向对 0 不起作用。

+-------------------+-------------+--------------+
|      Compile      | literal.cpp | variable.cpp |
+-------------------+-------------+--------------+
| g++               |          32 |           31 |
| g++ -O            |          32 |           32 |
| g++ -march=native |          32 |           32 |
+-------------------+-------------+--------------+

文字.cpp
#include <iostream>

int main(){
    int i = 0;
    std::cout << __builtin_clz(0) << std::endl;
}

变量.cpp
#include <iostream>

int main(){
    int i = 0;
    std::cout << __builtin_clz(i) << std::endl;
}

g++ -S [in name] -o [out name] 的差异
1c1
<       .file   "literal.cpp"
---
>       .file   "variable.cpp"
23c23,26
<       movl    $32, %esi
---
>       movl    -4(%rbp), %eax
>       bsrl    %eax, %eax
>       xorl    $31, %eax
>       movl    %eax, %esi

g++ 的差异 -march=native -S [in name] -o [out name]
1c1
<       .file   "literal.cpp"
---
>       .file   "variable.cpp"
23c23,25
<       movl    $32, %esi
---
>       movl    -4(%rbp), %eax
>       lzcntl  %eax, %eax
>       movl    %eax, %esi

g++ -O -S [in name] -o [out name] 的差异
1c1
<       .file   "literal.cpp"
---
>       .file   "variable.cpp"

最佳答案

当您在禁用优化的情况下编译时,编译器不会跨语句进行常量传播。那部分是 Why does integer division by -1 (negative one) result in FPE? 的拷贝 - 在那里阅读我的答案,和/或 Why does clang produce inefficient asm with -O0 (for this simple floating point sum)?

这就是为什么文字零可以与值 = 0 的变量不同的原因。
只有禁用优化的变量会导致运行时 bsr+xor $31, %reg

正如 in the GCC manual 记录的 __builtin_clz


这允许 clz/ctz 在 x86 上分别编译为 31- bsrbsf 指令。 31-bsr 是用 bsr + xor $31,%reg 实现的,这要感谢 2 的补码。 (BSR 产生最高设置位的索引,而不是前导零计数)。

请注意,它只表示结果,而不是行为。它不是 C++ UB (整个程序绝对可以做任何事情),它仅限于该结果,就像在 x86 asm 中一样。但无论如何,似乎当输入是编译时常量 0 时,GCC 会像 x86 lzcnt 一样生成类型宽度,并且像其他 ISA 上的 clz 指令一样。 (这可能发生在与目标无关的 GIMPLE 树优化中,其中通过包括内置函数在内的操作进行常量传播。)

Intel 文档 bsf / bsr as 如果内容源操作数为 0,则目标操作数的内容未定义。在现实生活中,英特尔硬件实现了与 AMD 文档相同的行为:在这种情况下保持目的地不变。

但是由于英特尔拒绝记录它,编译器不会让您编写利用它的代码。 GCC 不知道也不关心这种行为,也没有提供利用它的方法。 MSVC 也没有,即使它的内在函数需要一个输出指针 arg 所以很容易以这种方式工作。见 VS: unexpected optimization behavior with _BitScanReverse64 intrinsic

使用 -march=native ,GCC 可以直接使用 BMI1 lzcnt ,这对于包括 0 在内的每个可能的输入位模式都有明确定义。它直接产生一个前导零计数而不是第一个设置位的索引。

(这就是为什么 BSR/BSF 对 input=0 没有意义;他们没有索引可以找到。有趣的事实:bsr %eax, %eaxeax=0 做“工作”。在 asm 中,指令还根据输入是否设置 ZF为零,因此您可以在 bsr 之前检测输出何时“未定义”而不是单独的 test+branch。或者在 AMD 和现实生活中的其他所有内容上,它使目标保持不变。)

进一步阅读 bsf 与 lzcnt 和 false 依赖项

在 Intel 上,直到 Skylake,lzcnt/tzcnt 对输出寄存器有一个错误的依赖,即使结果从不依赖它。 IIRC,Coffee Lake 还修复了 popcnt 的错误 dep。 (所有这些都在与 BSR/BSF 相同的执行单元上运行。)

  • Why does breaking the "output dependency" of LZCNT matter?
  • VS: unexpected optimization behavior with _BitScanReverse64 intrinsic
  • How can x86 bsr/bsf have fixed latency, not data dependent? Doesn't it loop over bits like the pseudocode shows?
  • 关于c++ - 文字 0 和 0 作为变量如何使用 __builtin_clz 函数产生不同的行为?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/59014816/

    10-11 15:29