【问题标题】:How can a literal 0 and 0 as a variable yield different behavior with the function __builtin_clz?作为变量的文字 0 和 0 如何使用函数 __builtin_clz 产生不同的行为?
【发布时间】:2020-03-19 17:46:09
【问题描述】:

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

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

我参加了建筑课程,但不了解差异程序集。看起来当给定文字值 0 时,即使没有优化,程序集也总是有 32 个硬编码的正确答案。使用 -march=native 时计算前导零的方法不同。

This post 关于用_BitScanReverse 模拟__builtin_clzbsrl %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;
}

变量.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"

【问题讨论】:

  • @Matt 未定义行为意味着所有赌注都已取消。任何事情都有可能发生。出人意料的结果毫无意义。
  • 请注意bsrl %eax,%eax 确实适用于eax=0,但结果是目标寄存器保持不变并根据输入设置ZF i> 为零。这是why bsr has a false dependency。 AMD 记录了 Intel 和 AMD 实现的 dst-unmodified 行为,但 Intel 没有;他们说输出寄存器的值是任意的。 felixcloutier.com/x86/bsr。 (但不像 C++ UB;它不会导致周围指令出现不可预知的行为。)

标签: c++ gcc assembly undefined-behavior intrinsics


【解决方案1】:

当您在禁用优化的情况下进行编译时,编译器不会跨语句进行常量传播。那部分是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 与 value = 0 的变量不同的原因。 只有禁用优化的变量会在运行时产生bsr+xor $31, %reg


如记录在 in the GCC manual__builtin_clz

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

这允许 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 记录为 如果内容源操作数为 0,则目标操作数的内容未定义。 在现实生活中,Intel 硬件实现了与 AMD 文档相同的行为:在这种情况下保持目的地不变。

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


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

(这就是为什么 BSR/BSF 对 input=0 没有意义;没有可供他们查找的索引。有趣的事实:bsr %eax, %eaxeax=0 有效。在 asm 中,说明也根据输入是否为零来设置 ZF,这样您就可以检测输出何时为“未定义”,而不是 bsr 之前的单独测试+分支。或者在 AMD 和现实生活中的其他所有东西上,它没有修改目的地。)


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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-10-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多