【问题标题】:GCC: Wrong compile-time evaluation of __builtin_ctz in some situations with -O2 and -O3GCC:在使用 -O2 和 -O3 的某些情况下,__builtin_ctz 的编译时评估错误
【发布时间】:2020-11-05 10:14:26
【问题描述】:

在过去的几个小时里,我一直在调试一个奇怪的问题,它只发生在发布版本 (-O3) 中,而不是在调试版本中(-g 并且没有优化)。最后,我可以将其归结为“计数尾随零”内置给我错误的结果,现在我想知道我是否刚刚发现了一个 GCC 错误,或者我是否遗漏了一些东西。

简短的故事是,显然,GCC 在某些情况下用 -O2 和 -O3 错误地评估 __builtin_ctz,但它在没有优化或 -O1 的情况下表现良好。这同样适用于长变量__builtin_ctzl__builtin_ctzll

我最初的假设是 __builtin_ctz(0) 应该解析为 32,因为它是内置的 unsigned int(32 位)版本,因此有 32 个尾随零位。我没有发现任何说明这些内置函数因输入为零而未定义,而与它们的实际工作让我确信它们不是。

让我们看一下我现在要讲的代码:

bool test_basic;
bool test_ctz;
bool test_result;

int ctz(const unsigned int x) {
    const int q = __builtin_clz(x);
    test_ctz = (q == 32);
    return q;
};

int main(int argc, char** argv) {
    {
        const int q = __builtin_clz(0U);
        test_basic = (q == 32);
    }
    {
        const int q = ctz(0U);
        test_result = (q == 32);
    }
    
    std::cout << "test_basic=" << test_basic << std::endl;
    std::cout << "test_ctz=" << test_ctz << std::endl;
    std::cout << "test_result=" << test_result << std::endl;
}

代码基本上做了三个测试,将结果存储在这些布尔值中:

  1. 如果 __builtin_clz(0U) 解析为 32,test_basic 为真。
  2. 如果__builtin_clz(x) 在函数ctz 中等于32,则test_ctz 为真。
  3. 如果ctz(0) 的结果等于 32,则test_result 为真。

因为我在 main 函数中调用了一次 ctz 并将零传递给它,所以我希望在程序结束时所有三个布尔值都为真。如果我在没有任何优化或-O1 的情况下编译它,实际上就是这种情况。但是,当我用-O2 编译它时,test_ctz 变为 false。我咨询了Compiler Explorer 以了解到底发生了什么。 (请注意,我自己使用的是 g++ 7.5,但我也可以使用任何更高版本来重现它。在 Compiler Explorer 中,我选择了它必须提供的最新版本,即 10.2。)

我们先看一下代码compiled with -O1。我看到 test_ctz 被简单地设置为 1。我猜这是因为这些内置函数被视为 constexpr 并且整个相当简单的函数 ctz 在编译时被评估。结果是正确的(在我最初的假设下),所以我很好。

那么从这里可能会出现什么问题呢?好吧,让我们看一下代码compiled with -O2。没有太大变化,只是 test_ctz 现在设置为 0!就是这样,超出了任何逻辑:编译器显然将 q == 32 评估为假,但随后从函数返回 q,我们将其与 32 进行比较,突然它为真(test_result)。我对此没有任何解释。我错过了什么吗?我是否发现了一些恶魔般的 GCC 错误?

如果您在设置test_ctz 之前printf q 的值会变得更有趣:然后控制台打印 32,因此计算实际上按预期工作 - 在运行时。然而在编译时,编译器认为 q 不是 32 并且 test_ctz 被强制为假。实际上,如果我将 q 的声明从 const int 更改为 volatile int 并因此在运行时强制计算,一切都会按预期工作,所以幸运的是有一个简单的解决方法。

最后,我想指出,我还使用了“计数前导零”内置函数(__builtin_clz 和长版本),在那里我无法观察到同样的问题;他们工作得很好。

【问题讨论】:

    标签: gcc compiler-optimization


    【解决方案1】:

    我没有找到任何说明这些内置函数未定义的输入为零

    你怎么能错过???来自gcc online docs other builtins

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

    返回 x 中尾随 0 位的数量,从最低有效位位置开始。 如果 x 为 0,则结​​果未定义。

    那么从这里可能会出现什么问题?

    在 99% 的情况下,不同优化级别的代码表现不同,这清楚地表明了代码中未定义的行为。在这种情况下,编译器优化会做出与架构指令 BSR 不同的决定,如果编译器在 x86 架构上生成 BSR,则结果仍然未定义,来自链接 If the content source operand is 0, the content of the destination operand is undefined。哦,还有LZCNT,在这种情况下你会得到LZCNT will produce the operand size when the input operand is zero,这可能更好地解释了你的代码的行为。

    我错过了什么吗?

    是的。您错过了 __builtin_ctz(0) 未定义。

    我是否发现了一些恶魔般的 GCC 错误?

    没有。

    我想指出,我还使用了“计数前导零”内置函数(__builtin_clz 和长版本)我无法在那里观察到同样的问题;他们工作得很好。

    可以在 gcc 文档中看到 __builtin_clz(0) 也是未定义的行为。

    【讨论】:

    • 好吧,老实说,我不知道我怎么能错过。非常感谢。
    • 现在我意识到我是多么想念它:(1)我认为它只是 LZCNT,正如你所说,如果输入为零,它会产生操作数大小 - 正是我需要的 - 和( 2) 我只对长版本真正感兴趣,但是没有为 0 定义内置函数的注释仅适用于 unsigned int 版本。长版本的描述只是指那些,坦率地说,我没有读过。对于我的最小示例,我仍然选择使用该核心版本。所以,是的,这是 RTFM 的经典案例,但至少看起来我可以为自己辩护。 :-)
    猜你喜欢
    • 2020-01-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-04-08
    • 1970-01-01
    • 2018-04-06
    • 2021-11-28
    • 2012-01-14
    相关资源
    最近更新 更多