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

Mat*_*att 4 c++ assembly gcc intrinsics undefined-behavior

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

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

我上过架构课程,但不了解差异化的程序集。看起来当给定字面值0时,即使不进行优化,该汇编总会以某种方式始终具有32个硬编码的正确答案。使用-march = native时,用于计算前导零的方法也不同。

这篇文章关于模拟__builtin_clz_BitScanReverse和行bsrl %eax, %eax似乎意味着位扫描反向不起作用0。

+-------------------+-------------+--------------+
|      Compile      | literal.cpp | variable.cpp |
+-------------------+-------------+--------------+
| g++               |          32 |           31 |
| g++ -O            |          32 |           32 |
| g++ -march=native |          32 |           32 |
+-------------------+-------------+--------------+
Run Code Online (Sandbox Code Playgroud)

literal.cpp

#include <iostream>

int main(){
    int i = 0;
    std::cout << __builtin_clz(0) << std::endl;
}
Run Code Online (Sandbox Code Playgroud)

variable.cpp

#include <iostream>

int main(){
    int i = 0;
    std::cout << __builtin_clz(i) << std::endl;
}
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

g ++的差异-march = native -S [输入名称] -o [输出名称]

1c1
<       .file   "literal.cpp"
---
>       .file   "variable.cpp"
23c23,25
<       movl    $32, %esi
---
>       movl    -4(%rbp), %eax
>       lzcntl  %eax, %eax
>       movl    %eax, %esi
Run Code Online (Sandbox Code Playgroud)

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

1c1
<       .file   "literal.cpp"
---
>       .file   "variable.cpp"
Run Code Online (Sandbox Code Playgroud)

Pet*_*des 5

在禁用优化的情况下进行编译时,编译器不会在语句之间进行常量传播。这部分是为什么FPE会被-1除以整数(负数)的结果的重复-在那里阅读我的答案,和/或为什么clang用-O0产生效率低的asm(对于这个简单的浮点数总和)?

这就是为什么字面零与值= 0 的变量可以不同的原因。只有禁用优化的变量才能在运行时生成bsr+xor $31, %reg


GCC手册中所述__builtin_clz

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

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

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


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

但是由于英特尔拒绝对其进行文档化,因此编译器不允许您编写利用此文档的代码。GCC不了解或不关心这种行为,因此无法提供利用它的方法。即使MSVC的内在函数需要一个输出指针arg,MSVC也不会这样做,因此很容易以这种方式工作。请参阅VS:_BitScanReverse64固有的意外优化行为


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

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


进一步阅读有关bsf与lzcnt和错误依赖关系的信息

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


归档时间:

查看次数:

90 次

最近记录:

5 年,9 月 前