【问题标题】:Is this a clang optimizer bug or an undefined behavior in C?这是 Clang 优化器错误还是 C 中未定义的行为?
【发布时间】:2015-11-01 01:43:29
【问题描述】:

此代码为 -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 标准来看,遍历所有 uint32_t 值的 do 循环是有效的,因为最大无符号整数值的增量被明确定义为为零。

涉及无符号操作数的计算永远不会溢出,因为 无法用生成的无符号整数表示的结果 type 以比最大数大一的数为模减少 可以由结果类型表示的值。

【问题讨论】:

  • @black vec 不在堆栈中
  • 0xb9fe2f2fedf7ebbd 太大而不能成为int,您应该添加ULL。
  • 无法在我的 32 位系统上使用 gcc-5.1.1 或 clang3.6.0 进行复制。
  • @mch:ULL 后缀不是必需的。十六进制整数常量的类型是int、unsigned int、long int、unsigned long int、long long int、unsigned long long int 中的第一个,其中可以表示其值。 0xb9fe2f2fedf7ebbd 在具有 64 位 long 的系统上属于 unsigned long int 类型,在具有 32 位 long 的系统上属于 unsigned long long int。
  • 生成的代码有什么不同?我看到的唯一问题是uint64_t 的 printf 格式。使用:printf("%" PRIu64 "%" PRIu64 "\n", count, loops);(如果 uint_64t 与 ull 相同,则可能没有任何区别)。

标签: c optimization clang c99


【解决方案1】:

我有理由确定这是 clang 中的一个错误。我在程序中没有看到未定义的行为(假设它没有超过实现的容量限制)——除了我将在下面解决的 printf 调用中的一个小问题(现在已经在编辑中解决了)问题)。我可能错过了什么,但我不这么认为。

如果我遗漏了什么,我希望它很快就会被指出。如果几天后这个答案仍然没有矛盾,我会认为它确实是clang中的一个错误。

更新: 原始发布者 Mark Adler 已报告此问题并确认这是 3.6.0 之前的 clang 中的一个错误,已在后续版本中更正。我会无耻地从his answer中窃取this link to the bug report。

正确的输出是:

47 bits set (4294967296 loops)

解决一些已经指出的(或者我自己注意到的):

static uint64_t vec[1 << 26] = {0};

这是一个大对象(229 字节,或半 GB,假设 CHAR_BIT==8),但它显然没有超过实现的容量。如果是这样,它将被拒绝。我不能 100% 确定标准是否需要这样做,但由于程序在较低的优化级别下可以正常工作,我们可以假设对象不是太大。

vec[31415927] = 0xb9fe2f2fedf7ebbd

常量0xb9fe2f2fedf7ebbd 不是问题。它的值在263和264之间,所以在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 的类型定义,我收到警告)。但这不太可能导致任何实际问题(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,无论有无 -m32,我都能得到正确的输出:-O0、-O1、-O2 和 -O3。时序表明 gcc 在任何优化级别都不会消除循环。

在同一系统上使用 clang 3.4,我得到正确的输出 -O0 或 -O1,但不正确的输出 (0 bits set) -O2 或 -O3。时序表明循环在-O2 和-O3 处被消除。当我使用clang -m32 编译时,所有优化级别的输出都是正确的(并且没有消除循环)。

当我将loops 的声明更改为

volatile uint64_t loops = 0;

我在所有优化级别都得到了正确的输出(并且没有消除循环)。

对程序的进一步调整(此处未显示)表明 vec[31415927] 确实设置为 0xb9fe2f2fedf7ebbd,即使优化产生了错误的位数。

【讨论】:

  • 谢谢。顺便说一句,我更新了 printf,并在问题中添加了不必要的常量大小。
  • 请注意:我无法使用更新的 clang(3.8.0 快照 20150720)重现该问题。生成的代码似乎适用于 -O0、-O1、-O2 和 -O3。
【解决方案2】:

它看起来确实像 clang 中的一个错误。我可以在运行 clang3.4-1ubuntu3 的 64 位系统中重现这一点;正如另一个答案所提到的,我总是使用 gcc 得到正确的输出(它从不优化循环),但如果我们使用 -O2 和 -O3,clang 似乎优化了循环。

这个答案并没有为 Keith 的彻底和出色的答案增加太多,但为了将来参考,我想展示一个可能的解决方法(volatile 除外)。

确实,将x、count 或loops 设置为 volatile 可以解决此问题,但经过一些试验后,我确定该错误似乎仅在 do { ... } while; 循环中表现出来。

如果您更改代码以使用while 或for 循环(并进行适当的更改以保持程序的行为),clang 将始终产生正确的输出并且循环不会被优化掉(但它-O3 仍然运行得更快)。

这是一个例子:

#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 = vec[0] & (uint64_t)1;
    uint64_t loops = 1;
    uint32_t x = 1;

    while (x) {
        if (vec[x >> 6] & ((uint64_t)1 << (x & 0x3f)))
            count++;
        x++;
        loops++;
    }

    printf("%" PRIu64 " bits set (%" PRIu64 " loops)\n", count, loops);
    return 0;
}

【讨论】:

    【解决方案3】:

    这是一个bug in pre-3.6.0 clang。 (“3.6.0svn”版本在 3.6.0 之前。)由于它已经在五个月前的 3.6.0 版本中修复,我已经向 Apple 报告了这个错误——这仍然是他们最近发布的编译器工具。

    【讨论】:

      猜你喜欢
      • 2012-09-28
      • 1970-01-01
      • 1970-01-01
      • 2018-09-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-01-09
      相关资源
      最近更新 更多