GCC:在使用 -O2 和 -O3 的某些情况下 __builtin_ctz 的错误编译时评估
GCC: Wrong compile-time evaluation of __builtin_ctz in some situations with -O2 and -O3
过去几个小时我一直在调试一个奇怪的问题,它只出现在发布版本 (-O3) 中,而不是出现在调试版本中(-g 并且没有优化)。最后,我可以将其归结为内置的“计数尾随零”给出了错误的结果,现在我想知道我是刚刚发现了 GCC 错误还是遗漏了什么。
简而言之,GCC 显然 __builtin_ctz
错误地使用 -O2 和 -O3 在某些情况下 ,但它在没有优化或 -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
变成假的。我咨询了 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
被强制为 false。事实上,如果我将 q
的声明从 const int
更改为 volatile int
并因此强制在运行时进行计算,一切都会按预期进行,所以幸运的是有一个简单的解决方法。
总而言之,我想指出的是,我还使用了“计数前导零”内置函数(__builtin_clz
和长版本),但我无法在那里观察到同样的问题;他们工作得很好。
I have not found anything stating that these builtins are undefined for the input being zero
你怎么能错过???来自 gcc online docs other builtins:
Built-in Function: int __builtin_ctz (unsigned int x)
Returns the number of trailing 0-bits in x, starting at the least significant bit position. If x is 0, the result is undefined.
So what could possibly go wrong from here?
在 99% 的情况下,不同优化级别的代码表现不同,这清楚地表明代码中存在未定义的行为。在这种情况下,编译器优化与体系结构指令 BSR and in case the compiler generates the BSR
on x86 architecture, the result is still undefined, from the link If the content source operand is 0, the content of the destination operand is undefined
. Och, there's also LZCNT 做出不同的决定,在这种情况下,您将得到 LZCNT will produce the operand size when the input operand is zero
,这可能更好地解释您的代码的行为。
Am I missing something?
是的。您缺少 __builtin_ctz(0)
未定义。
Have I found some demonical GCC bug?
没有
I'd like to note that I also use the "count leading zeroes" builtins (__builtin_clz and long versions) I could not observe the same problem there; they work just fine.
在gcc docs中可以看到__builtin_clz(0)
也是未定义的行为
过去几个小时我一直在调试一个奇怪的问题,它只出现在发布版本 (-O3) 中,而不是出现在调试版本中(-g 并且没有优化)。最后,我可以将其归结为内置的“计数尾随零”给出了错误的结果,现在我想知道我是刚刚发现了 GCC 错误还是遗漏了什么。
简而言之,GCC 显然 __builtin_ctz
错误地使用 -O2 和 -O3 在某些情况下 ,但它在没有优化或 -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;
}
代码基本上做了三个测试,将结果存储在那些布尔值中:
-
如果
test_basic
为真。
如果 test_ctz
为真 如果ctz(0)
的结果等于 32,则test_result
为真。
__builtin_clz(0U)
解析为 32,则 __builtin_clz(x)
在函数 ctz
. 中等于 32,则 因为我在 main
函数中调用了一次 ctz
并将零传递给它,所以我希望所有三个布尔值在程序结束时都为真。如果我在没有任何优化或 -O1
的情况下编译它,实际上就是这种情况。但是,当我用 -O2
编译它时,test_ctz
变成假的。我咨询了 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
被强制为 false。事实上,如果我将 q
的声明从 const int
更改为 volatile int
并因此强制在运行时进行计算,一切都会按预期进行,所以幸运的是有一个简单的解决方法。
总而言之,我想指出的是,我还使用了“计数前导零”内置函数(__builtin_clz
和长版本),但我无法在那里观察到同样的问题;他们工作得很好。
I have not found anything stating that these builtins are undefined for the input being zero
你怎么能错过???来自 gcc online docs other builtins:
Built-in Function: int __builtin_ctz (unsigned int x)
Returns the number of trailing 0-bits in x, starting at the least significant bit position. If x is 0, the result is undefined.
So what could possibly go wrong from here?
在 99% 的情况下,不同优化级别的代码表现不同,这清楚地表明代码中存在未定义的行为。在这种情况下,编译器优化与体系结构指令 BSR and in case the compiler generates the BSR
on x86 architecture, the result is still undefined, from the link If the content source operand is 0, the content of the destination operand is undefined
. Och, there's also LZCNT 做出不同的决定,在这种情况下,您将得到 LZCNT will produce the operand size when the input operand is zero
,这可能更好地解释您的代码的行为。
Am I missing something?
是的。您缺少 __builtin_ctz(0)
未定义。
Have I found some demonical GCC bug?
没有
I'd like to note that I also use the "count leading zeroes" builtins (__builtin_clz and long versions) I could not observe the same problem there; they work just fine.
在gcc docs中可以看到__builtin_clz(0)
也是未定义的行为