作为变量的立即数0和0如何与__builtin_clz函数产生不同的行为?

问题描述 投票:3回答:1

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

当我使用文字值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, %esi
---
>       movl    -4(%rbp), %eax
>       lzcntl  %eax, %eax
>       movl    %eax, %esi

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

1c1
<       .file   "literal.cpp"
---
>       .file   "variable.cpp"
c++ gcc assembly undefined-behavior intrinsics
1个回答
4
投票

当禁用优化功能进行编译时,编译器不会在语句之间进行常数传播。该部分是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所示>

从最高有效位位置开始,返回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树优化中,其中通过包含内置函数的操作进行了恒定传播。)

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

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


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

(这就是为什么BSR / BSF对于input = 0毫无意义;没有索引可供他们查找。有趣的事实:lzcntlzcnt起作用。在asm中,指令也设置为ZF根据输入是否为零,因此您可以检测到输出何时为“未定义”,而不是在0之前进行单独的test +分支。或者在AMD和现实生活中的所有其他事情上,其未更改目标。) >


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

在直到Skylake的Intel上,bsr %eax, %eax / eax=0对输出寄存器都有错误的依赖性,即使结果与not无关。 IIRC,Coffee Lake还修复了bsr的错误深度。 (所有这些都与BSR / BSF在同一执行单元上运行。)

  • lzcnt
  • tzcnt
  • popcnt
© www.soinside.com 2019 - 2024. All rights reserved.