【问题标题】:Different gcc output for __builtin_clzll on different optimisation levels and wrapped in a function__builtin_clzll 在不同优化级别上的不同 gcc 输出并包装在一个函数中
【发布时间】:2017-07-14 16:28:45
【问题描述】:

我对以下代码的行为/输出感到困惑,要么这是一个错误,要么我遗漏了一些东西。 (天湖拱门上的 Ubuntu 16.04)

#include <iostream>

int wrap(unsigned long long val) {
    return __builtin_clzll(val);
} 

using namespace std;
int main() {
    cout << __builtin_clzll(0) << " " << wrap(0) << endl;
    cout << __builtin_clzll(1) << " " << wrap(1) << endl;
    cout << __builtin_clzll(2) << " " << wrap(2) << endl;
}

这里是不同编译设置的不同输出。我知道如果通过零, clz 可能会返回未定义的结果。然而,直接内联调用总是可以正常工作,但是一旦涉及堆栈,编译器就会搞砸。

snk@maggy:~/HCS$ g++ -O0 test.cpp -o test
snk@maggy:~/HCS$ ./test
64 4196502
63 63
62 62
snk@maggy:~/HCS$ 

-O > 0 级别不会改变结果,我猜 gcc 是内联的。这是预期的结果...

snk@maggy:~/HCS$ g++ -O1 test.cpp -o test
snk@maggy:~/HCS$ ./test
64 64
63 63
62 62

使用 -mlzcnt 会变得更好:

snk@maggy:~/HCS$ g++ -O0 -mlzcnt test.cpp -o test
snk@maggy:~/HCS$ ./test
64 0
63 0
62 1

snk@maggy:~/HCS$ g++ -O1 -mlzcnt test.cpp -o test
snk@maggy:~/HCS$ ./test
64 64
63 63
62 62

snk@maggy:~/HCS$ g++ --version
g++ (Ubuntu 5.4.0-6ubuntu1~16.04.4) 5.4.0 20160609
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

谢谢, 码

【问题讨论】:

  • 您不应该期望未定义的行为会导致任何特定的结果(甚至两次相同的结果都不会)。这就是它未定义的原因。
  • 未定义的结果或行为通常意味着整个程序不再可靠。
  • 我怀疑在优化时,__builtin_clzll(&lt;constant&gt;) 正在编译时被评估 - 并试图提供一个合理的答案,尽管文档将结果描述为 undefined。没有异议,4196502 的值虽然令人费解。
  • __builtin_clzll 是 GCC 的优点,只处理非 0 值。 Intel 的LZCNT 是 BMI 的一部分,可以处理 0 值。
  • 你错过了一个要点。您没有阅读文档。 对于零结果是 UB。

标签: c linux gcc


【解决方案1】:

这个问题中有趣的案例是-mlzcnt 的行为。这在 2013 年被报告为 GCC bug 58928,但该错误报告后来被撤回,因为当您为不支持 LZCNT 操作码的 Intel CPU 提供 -mlzcnt 时,这是“预期的”行为。

事实证明,LZCNT 是带有 F3 前缀的 BSR(反向位搜索);在没有实现 LZCNT 的 Intel CPU 上,它不会被捕获为无效的操作码,而是被解释为 BSR,它返回 1 位的位位置(位 0 是低位),而不是前面 0 的个数。

如前所述,使用参数 0 调用 __builtin_clz 会产生未定义的行为。您不应该对未定义行为的结果抱有任何期望;甚至两次都是相同的结果。

【讨论】:

    【解决方案2】:

    根据GCC documentation for built-in functions(添加了粗体字)

    内置函数:int __builtin_clz (unsigned int x)

    返回 x 中前导 0 位的数量,最多开始 重要位的位置。 如果 x 为 0,则结​​果未定义

    ...

    内置函数:int __builtin_clzll (unsigned long long)

    类似于__builtin_clz,除了参数类型是unsigned long long

    0 的结果未定义。

    【讨论】:

    • OP知道这一点,并已将其声明为错误的潜在来源。
    【解决方案3】:

    让我们看看wrap没有优化:

            .globl wrap(unsigned long long)
    wrap(unsigned long long):
    
            pushq   %rbp
            movq    %rsp, %rbp
    
            movq    %rdi, -8(%rbp)
            bsrq    -8(%rbp), %rax
            xorq    $63, %rax
    
            popq    %rbp
            ret
    

    如果源是 (0),AMD 和 Intel 都不要修改目标寄存器 - 尽管我相信只有 AMD 记录了这种行为。实际上,您将获得之前设置的任何%rax(它不是调用者保存的寄存器——它用于某些返回值),与(63) 异或。所以%rax 可以设置为一些“以前的”结果。甚至可能受制于address space randomization 的结果。

    这个函数的堆栈不是罪魁祸首。 x86-64 ELF(和 OS X Mach-O)ABI 中的第一个参数在 %rdi 中传递。缺乏优化会将其溢出到堆栈帧上的内存中,但它仍将(0) 存储在-8(%rbp) 此处。

    TL;DR - %rax 在条目中包含不相关的垃圾,并且不会被 (0) 源操作数修改为 bsrxor 可能会翻转几个低位。

    【讨论】:

      猜你喜欢
      • 2020-07-06
      • 2012-01-04
      • 2013-04-27
      • 1970-01-01
      • 2013-03-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多