【问题标题】:Why don't modern C++ compilers optimize away simple loops like this? (Clang, MSVC)为什么现代 C++ 编译器不优化这样的简单循环? (Clang,MSVC)
【发布时间】:2014-09-28 20:44:59
【问题描述】:

当我使用 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,从而优化整个循环——更不用说增加变量已经未定义的事实行为。

然而只有 GCC 能够确定它是一个空操作,并且没有一个编译器告诉我这是一个未初始化的变量。

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

【问题讨论】:

  • 除了堆栈分配和内存中无意义位置的递增外,循环中没有副作用,除非使用关键字volatile,否则编译器可以优化掉这些。 clang 没有看到执行循环没有任何用处,这真的很奇怪......如果你删除内部循环,编译器是否设法看到什么都不做?
  • @Fors:您甚至不需要删除循环,只需将数组更改为单个变量,它就可以很好地优化。但在这种情况下,Clang 检测到变量未初始化,但 GCC 和 MSVC 似乎无法解决这个问题。
  • 无论是否存在循环,只要类型是数组,就好像根本没有警告。无论编译器标志如何,此代码都会在没有任何警告的情况下编译:int main(){ int x[1]; ++x[0];},在 Clang 和 g++ 上都
  • @Mehrdad 是的,我也看到了这个,确实很奇怪。另一方面,我不明白为什么未初始化的数组根本不会发出警告,即使在最简单的代码中也是如此。实际上我现在意识到对数组发出警告,但必须使用大于 0 的 -O 优化。
  • 我说的是int main(){ int x[1]; ++x[0];} 代码,它不会在-O0 上发出警告,但会在-O1-O2-O3 上发出警告。我使用 g++ (MacPorts gcc49 4.9-20140416_2) 4.9.0 20140416 (prerelease)。对于循环,只要未初始化在循环内,所有赌注都关闭,g++ 上没有更多警告,只有 clang。如果类型是数组,那么即使 clang 也不会发出警告。

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


【解决方案1】:

未定义的行为在这里无关紧要。将内部循环替换为:

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

... 具有相同的效果,至少对于 Clang。

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

与 n.m. 的回答相反,将 int 替换为 unsigned 并不能解决问题。 Clang 3.4.1 生成的代码using intusing unsigned int 是相同的。

【讨论】:

  • “未定义的行为在这里无关紧要。” ...这不是我从 n.m. 的回答中得到的印象。如果用无符号替换有符号会改变行为,那么 UB 必须是相关的,不是吗?
  • 在此示例中将所有 ints 替换为 unsigneds 不会改变行为。比较生成的代码beforeafter。 n.m.的回答是错误的。
  • +1 哇,我应该测试一下。我只是理所当然地认为他是对的,现在我看起来很愚蠢。现在在 Clang 3.6.0 (Windows) 上测试它,我也没有看到它发生......我想知道他使用的是什么标志,或者他是否不小心运行了 g++ 而不是 clang++......
  • 这里的行为也是未定义的。
  • @jthill 好点。但是,初始化数组在这里也没有任何区别。代码仍未删除。
【解决方案2】:

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

显然,不同的编译器使用不同的启发式算法; 编译器可以将数组视为单个对象,并检测 它从未影响输出(可观察的行为)。一些 然而,编译器可能会选择不这样做,理由是 通常,做很多工作却收效甚微:多久 这样的优化是否适用于实际代码?

【讨论】:

  • @Mehrdad 他的回答和我的回答是互补的。正如我在第二段中所说:不同的编译器使用不同的启发式方法来确定分析的内容和深度。我的回答只是要指出,他实际上在编译器上工作过,他所看到的并不让我感到惊讶。
  • 我明白,但我提到的原因是在我看来(从 nm 的答案中的 unsigned 事情)Clang 故意没有优化这个-- 即,编译器编写者有理由避免进行这种优化。编译器已经做了更复杂的死代码消除,所以当他们无法弄清楚这么简单的事情时,你真的只是继续说它是意外吗?我觉得有一个很好的理由不执行这个我不知道的优化。
  • @Mehrdad 同意。他们会优化unsigned 而不是int 似乎很奇怪。 (将类型更改为unsigned 不会消除未定义的行为;所有 变量必须在读取之前进行初始化。)看看编译器内部发生了什么会很有趣。
  • 嗯,你确定它是未签名的 UB 吗?我问是因为这个答案声称它对于无符号来说是明确定义的,而且对我来说这似乎是合乎逻辑的:stackoverflow.com/a/11965368/541686(编辑:我刚刚意识到答案只是关于 C,所以我不确定它是否适用于C++ 也是。)
  • @Mehrdad 在 C 和 C++ 中,对于所有非字符类型,访问未初始化的值都是未定义的行为。你引用的答案是完全错误的。 C 标准是最明确的(第 6.2.6/5 节):“某些对象表示不需要表示对象类型的值。如果对象的存储值具有这样的表示并且由不具有字符类型,行为未定义。”在 C++ 中,您必须从值表示可能不会使用对象表示中的所有位这一事实中推断出这一点。
【解决方案3】:
++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

【讨论】:

  • 我说它是 UB 的原因是不能保证它们不是INT_MAX,因此它们可能会溢出。因此编译器可以假设它们INT_MAX并且它们溢出,这是未定义的。
  • @Mehrdad 如果您添加a[j] = INT_MAX;,那么循环将与a[j] = 0; 一样被消除,因此clang 显然不会自行假设INT_MAX。我认为最好的解释是编译器试图保留硬件捕获行为。
【解决方案4】:

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-11-01
    • 2012-12-11
    • 1970-01-01
    • 1970-01-01
    • 2014-07-13
    • 2020-09-16
    • 2019-01-16
    相关资源
    最近更新 更多