这是一个clang优化器错误还是C中未定义的行为?

时间:2015-08-08 19:16:09

标签: c optimization clang c99

此代码为-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是以大于最大值的数量减少的模数   可以由结果类型表示的值。

3 个答案:

答案 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类型的参数; countloops的类型为uint64_t。在这里,根据实现,我们可能有未定义的行为(在我的系统uint64_t上是unsigned long的typedef,我收到警告)。但它不太可能导致任何实际问题(unsigned long longuint64_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的正确输出,但在-O10 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除外)。

事实上,制作xcountloops易变的其他任何东西都会修复它,但经过一些实验后,我确定该错误似乎仅在{{1}上显现出来}循环。

如果更改代码以使用do { ... } while;while循环(并进行适当的更改以维护程序的行为),则clang将始终生成正确的输出并且循环未优化离开(但for)仍然可以更快地运行。

以下是一个例子:

-O3