为什么不使用现代C ++编译器来优化这样的简单循环呢? (Clang,MSVC)

时间:2014-08-06 02:52:15

标签: c++ visual-c++ gcc clang compiler-optimization

当我使用Clang(-O3)或MSVC(/O2)编译并运行此代码时...

#include <stdio.h>
#include <time.h>

static int const N = 0x8000;

int main()
{
    clock_t const start = clock();
    for (int i = 0; i < N; ++i)
    {
        int a[N];    // Never used outside of this block, but not optimized away
        for (int j = 0; j < N; ++j)
        {
            ++a[j];  // This is undefined behavior (due to possible
                     // signed integer overflow), but Clang doesn't see it
        }
    }
    clock_t const finish = clock();
    fprintf(stderr, "%u ms\n",
        static_cast<unsigned int>((finish - start) * 1000 / CLOCKS_PER_SEC));
    return 0;
}

......循环没有得到优化。

此外,既不 Clang 3.6 也不 Visual C ++ 2013 也不 GCC 4.8.1告诉我该变量未初始化!

现在我意识到缺乏优化并不是一个错误本身,但我发现这令人惊讶,因为编译器现在应该是非常聪明的。这看起来就像这么简单的一段代码,即使是十年前的活体分析技术也应该能够优化掉变量a,从而整个循环 - 更别说增加变量的事实是已经未定义的行为。

然而,只有海湾合作委员会能够发现它是无操作的,并且没有编制者告诉我这是一个未初始化的变量。

这是为什么?是什么阻止简单的活跃度分析告诉编译器a未使用?此外,为什么编译器首先检测不到a[j]未初始化?为什么所有这些编译器中现有的未初始化变量检测器都无法捕获这个明显的错误?

4 个答案:

答案 0 :(得分:6)

未定义的行为与此无关。用以下内容替换内循环:

    for (int j = 1; j < N; ++j)
    {
        a[j-1] = a[j];
        a[j] = j;
    }

......具有相同的效果,至少与Clang一样。

问题是内部循环既从a[j]加载(对于某些j)又加载到a[j](对于某些j)。没有任何商店可以删除,因为编译器认为它们可能在以后加载时可见,并且没有任何加载可以被删除,因为它们的值被使用(作为后期商店的输入)。因此,循环仍然会对内存产生副作用,因此编译器看不到它可以被删除。

与上午的答案相反,将int替换为unsigned并不会使问题消失。 Clang 3.4.1 using intusing unsigned int生成的代码完全相同。

答案 1 :(得分:2)

这是一个有关优化的有趣问题。我会 期望在大多数情况下,编译器会处理每个元素 在执行死代码时,数组作为单个变量 分析。 Ans 0x8000使得个别变量太多 跟踪,所以编译器不尝试。事实a[j] 并不总是访问同一个对象可能会导致问题 以及优化器。

显然,不同的编译器使用不同的启发式方法; 编译器可以将数组视为单个对象,并进行检测 它从未影响输出(可观察的行为)。一些 但是,编制者可以选择不这样做 通常情况下,很多工作都是为了获得很少的收益:多久一次 这样的优化是否适用于实际代码?

答案 2 :(得分:2)

++a[j];  // This is undefined behavior too, but Clang doesn't see it

您是说这是未定义的行为,因为数组元素未初始化?

如果是这样,虽然这是对标准4.1 / 1的通用解释,但我认为这是不正确的。程序员通常使用这个术语,这些元素是“未初始化的”,但我不认为这与C ++规范对该术语的使用完全一致。

特别是C ++ 11 8.5 / 11声明这些对象实际上是默认初始化的,在我看来这与未初始化是互斥的。该标准还规定,对于某些默认初始化的对象,意味着“不执行初始化”。有些人可能认为这意味着它们未初始化但未指定,我只是认为它不需要这样的性能。

规范确实表明数组元素将具有不确定的值。 C ++通过引用C标准指定,不确定值可以是有效表示,可以是合法访问,也可以是陷阱表示。如果数组元素的特定不确定值恰好都是有效表示,(并且没有INT_MAX,避免溢出),那么上面的行不会在C ++ 11中触发任何未定义的行为。

由于这些数组元素可能是陷阱表示,因此clang的行为完全符合它们,因为它们可以保证是陷阱表示,有效地选择创建代码UB以创建优化机会。

即使clang没有这样做,它仍然可以选择根据数据流进行优化。 Clang确实知道如何做到这一点,事实证明,如果内部循环略有改变,那么循环就会被删除。

那么为什么UB的(可选)存在似乎阻碍了优化,当UB通常被视为更多优化的机会?

可能发生的事情是clang已经决定用户希望int根据硬件的行为进行陷阱。因此,clang必须生成能够在硬件中忠实再现程序行为的代码,而不是将陷阱作为优化机会。 这意味着无法根据数据流消除循环,因为这样做可能会消除硬件陷阱。


C ++ 14更新行为,以便访问不确定值本身会产生未定义的行为,与是否考虑未初始化的变量无关:https://stackoverflow.com/a/23415662/365496

答案 3 :(得分:1)

这确实非常有趣。我尝试了MSVC 2013的例子。 我的第一个想法是,++ a [j]有些未定义的事实是没有删除循环的原因,因为删除它肯定会将程序的含义从未定义/不正确的语义改为有意义的东西,所以我之前尝试初始化值,但循环仍然没有消失。

之后我替换了++ a [j]; a [j] = 0;然后产生没有任何循环的输出,因此删除了对clock()的两次调用之间的所有内容。我只能猜测原因。也许优化器无法证明操作符++因任何原因没有副作用。