【发布时间】: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;
}
代码基本上做了三个测试,将结果存储在这些布尔值中:
-
如果
__builtin_clz(0U)解析为 32,test_basic为真。 -
如果
__builtin_clz(x)在函数ctz中等于32,则test_ctz为真。 -
如果
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 和长版本),在那里我无法观察到同样的问题;他们工作得很好。
【问题讨论】: