使用变量__builtin_clz如何将文字0和0作为变量产生不同的行为?

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

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

我参加了一个建筑课程,但是不了解差异化的程序集。看起来,当给定文字值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 |
+-------------------+-------------+--------------+

literal.cpp

#include <iostream>

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

variable.cpp

#include <iostream>

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

g ++的差异-S [名称] -o [名称]

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 =本机-S [名称] -o [名称]

1c1
<       .file   "literal.cpp"
---
>       .file   "variable.cpp"
23c23,25
<       movl    $32,%eax
>       lzcntl  %eax,%esi

g ++的差异-O -S [名称] -o [名称]

1c1
<       .file   "literal.cpp"
---
>       .file   "variable.cpp"
l1120726 回答:使用变量__builtin_clz如何将文字0和0作为变量产生不同的行为?

在禁用优化的情况下进行编译时,编译器不会在语句之间进行常量传播。该部分是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中产生。


针对__builtin_clz的{​​{3}}记录

  

从最高有效位位置开始,返回x中前导0位的数目。 如果x为0,则结果不确定。

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

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


英特尔将in the GCC manual / bsf记录为如果内容源操作数为0,则目标操作数的内容未定义。在现实生活中,英特尔硬件实现了相同的功能行为AMD文件:在这种情况下,请保持目的地不变。

但是由于Intel拒绝对其进行文档化,因此编译器不会让您编写利用它的代码。 GCC不了解或不关心这种行为,因此无法提供利用它的方法。即使MSVC的内在函数需要一个输出指针arg,MSVC也不会这样做,因此很容易以这种方式工作。参见bsr


通过-march=native,GCC可以直接使用BMI1 VS: unexpected optimization behavior with _BitScanReverse64 intrinsic,对于每个可能的输入位模式,包括0 ,BMI1都已明确定义。它直接产生前导零计数,而不是第一个置位的 index

(这就是为什么BSR / BSF对input = 0毫无意义;没有索引可供他们查找。有趣的事实:bsr %eax,%eaxeax=0做了“工作”。在asm中,这些指令还会根据输入是否为零来设置ZF,以便您可以检测到输出何时是“未定义的”,而不是检测bsr之前的单独的test +分支;或者在AMD和现实生活中的所有其他情况下,保持目的地不变。)


进一步了解bs​​f与lzcnt和错误的依赖关系

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

本文链接:https://www.f2er.com/3043055.html

大家都在问