此代码为-O1和-O2提供了不同的结果:
/*
Example of a clang optimization bug.
Mark Adler, August 8, 2015.
Using -O0 or -O1 takes a little while and gives the correct result:
47 bits set (4294967296 loops)
Using -O2 or -O3 optimizes out the loop, returning immediately with:
0 bits set (4294967296 loops)
Of course, there weren't really that many loops. The number of loops was
calculated, correctly, by the compiler when optimizing. But it got the
number of bits set wrong.
This is with:
Apple LLVM version 6.1.0 (clang-602.0.53) (based on LLVM 3.6.0svn)
Target: x86_64-apple-darwin14.4.0
*/
#include <stdio.h>
#include <inttypes.h>
/* bit vector of 1<<32 bits, initialized to all zeros */
static uint64_t vec[1 << 26] = {0};
int main(void)
{
/* set 47 of the bits. */
vec[31415927] = UINT64_C(0xb9fe2f2fedf7ebbd);
/* count the set bits */
uint64_t count = 0;
uint64_t loops = 0;
uint32_t x = 0;
do {
if (vec[x >> 6] & ((uint64_t)1 << (x & 0x3f)))
count++;
x++;
loops++;
} while (x);
printf("%" PRIu64 " bits set (%" PRIu64 " loops)\n", count, loops);
return 0;
}
这是一个错误吗?或者在某种程度上存在未定义的行为,编译器是否有权为?
提供不同的结果据我所知,从C99标准来看,do
循环遍历所有uint32_t
值是有效的,因为最大无符号整数值的增量被很好地定义为零。
涉及无符号操作数的计算永远不会溢出,因为 结果无法由结果无符号整数表示 type是以大于最大值的数量减少的模数 可以由结果类型表示的值。
答案 0 :(得分:27)
我有理由相信这是铿锵的错误。我发现程序中没有未定义的行为(假设它没有超出实现的容量限制) - 除了printf
调用中的一个小问题,我将在下面解决(现在已经在问题的编辑中解决了这个问题)。我可能错过了什么,但我不这么认为。
如果我错过了什么,我希望很快就能指出。如果这个答案在几天之后仍然没有受到影响,我会把它作为一个强有力的迹象表明它确实是一个铿锵的错误。
更新:原始海报Mark Adler报告了这一情况并确认它是3.6.0之前版本中的错误,在以后的版本中进行了更正。我将从this link to the bug report无耻地窃取his answer。
正确的输出是:
47 bits set (4294967296 loops)
要解决一些已经指出的事情(或者我已经注意到自己):
static uint64_t vec[1 << 26] = {0};
这是一个大对象(2 29 字节,或半个千兆字节,假设为CHAR_BIT==8
),但它显然不超过实现的容量。如果确实如此,它将被拒绝。我不是100%确定标准需要这个,但由于程序在较低的优化级别下可以正常工作,我们可以假设对象不是太大。
vec[31415927] = 0xb9fe2f2fedf7ebbd
常量0xb9fe2f2fedf7ebbd
不是问题。其值在2 63 和2 64 之间,因此它在uint64_t
的范围内。十六进制整数常量的类型足够宽以保持其值(除非它超过ULLONG_MAX
,但这不是这里的情况。)
if (vec[x >> 6] & ((uint64_t)1 << (x & 0x3f)))
我简单地认为左移可能是一个问题,但它不是。左操作数的类型为uint64_t
,右操作数的范围为0
.. 63
。 64位的左移将具有未定义的行为,但这不是这种情况。
printf("%llu bits set (%llu loops)\n", count, loops);
问题的更新解决了以下问题。我已经尝试了代码的更新版本,并得到了相同的结果。
<击>
%llu
需要unsigned long long
类型的参数; count
和loops
的类型为uint64_t
。在这里,根据实现,我们可能有未定义的行为(在我的系统uint64_t
上是unsigned long
的typedef,我收到警告)。但它不太可能导致任何实际问题(unsigned long long
和uint64_t
通常具有相同的表示形式,即使它们不是同一类型),并且当我添加强制转换以避免任何UB:
printf("%llu bits set (%llu loops)\n",
(unsigned long long)count,
(unsigned long long)loops);
我得到了同样的行为。以下结果适用于已添加到printf
调用的强制转换的程序。
击>
在我的64位系统上使用gcc 5.2.0,我得到了-O0
,-O1
,-O2
和-O3
的正确输出,有或没有{ {1}}。时间表明gcc不会在任何优化级别消除循环。
在同一系统上使用clang 3.4,我得到了-m32
或-O0
的正确输出,但在-O1
或0 bits set
输出错误(-O2
) }。时间表示在-O3
和-O2
处消除了循环。当我使用-O3
进行编译时,在所有优化级别上输出都是正确的(并且没有消除循环)。
当我将clang -m32
的声明更改为
loops
我在所有优化级别获得正确的输出(并且不会消除循环)。
对程序的进一步调整(此处未显示)显示volatile uint64_t loops = 0;
确实设置为vec[31415927]
,即使优化产生了错误的位数。
答案 1 :(得分:16)
这是bug in pre-3.6.0 clang。 (“3.6.0svn”版本在3.6.0之前。)因为它已经在五个月前的3.6.0版本中得到修复,所以我向Apple报告了这个错误 - 这仍然是他们最新的编译器版本工具。
答案 2 :(得分:6)
它确实看起来像铿锵声中的一个错误。我可以在运行clang3.4-1ubuntu3的64位系统中重现这一点;正如另一个答案所提到的那样,我总是使用gcc得到正确的输出(它永远不会优化掉循环),但是如果我们使用-O2
和-O3
,clang似乎会优化掉循环。
这个答案并没有给Keith的全面和出色的答案增添太多,但为了将来的参考,我想展示一个可能的解决方法(volatile
除外)。
事实上,制作x
,count
或loops
易变的其他任何东西都会修复它,但经过一些实验后,我确定该错误似乎仅在{{1}上显现出来}循环。
如果更改代码以使用do { ... } while;
或while
循环(并进行适当的更改以维护程序的行为),则clang将始终生成正确的输出并且循环未优化离开(但for
)仍然可以更快地运行。
以下是一个例子:
-O3