只有 1 种情况下 __builtin_clz
给出了错误的答案。我很好奇是什么导致了这种行为。
当我使用文字值 0 时,我总是按预期得到 32。但是 0 作为变量产生 31。为什么存储值 0 的方法很重要?
我参加了架构类(class),但不了解不同的程序集。看起来当给定文字值 0 时,即使没有优化,程序集也会以某种方式始终具有 32 硬编码的正确答案。并且在使用 -march=native 时计算前导零的方法是不同的。
This post 关于用 __builtin_clz
模拟 _BitScanReverse
和 bsrl %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- bsr
或 bsf
指令。 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, %eax
为 eax=0
做“工作”。在 asm 中,指令还根据输入是否设置 ZF为零,因此您可以在 bsr
之前检测输出何时“未定义”而不是单独的 test+branch。或者在 AMD 和现实生活中的其他所有内容上,它使目标保持不变。)
进一步阅读 bsf 与 lzcnt 和 false 依赖项
在 Intel 上,直到 Skylake,lzcnt
/tzcnt
对输出寄存器有一个错误的依赖,即使结果从不依赖它。 IIRC,Coffee Lake 还修复了 popcnt
的错误 dep。 (所有这些都在与 BSR/BSF 相同的执行单元上运行。)
关于c++ - 文字 0 和 0 作为变量如何使用 __builtin_clz 函数产生不同的行为?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/59014816/