是否允许编译器优化堆内存分配?

Is the compiler allowed to optimize out heap memory allocations?

考虑以下使用 new 的简单代码(我知道没有 delete[],但它不属于这个问题):

int main()
{
    int* mem = new int[100];

    return 0;
}

是否允许编译器优化 new 调用?

在我的研究中,g++ (5.2.0) and Visual Studio 2015 do not optimize out the new call, while clang (3.0+) does。所有测试都是在启用完全优化的情况下进行的(-O3 用于 g++ 和 clang,发布模式用于 Visual Studio)。

new 是不是在后台进行系统调用,使得编译器不可能(并且非法)对其进行优化?

编辑:我现在已经从程序中排除了未定义的行为:

#include <new>  

int main()
{
    int* mem = new (std::nothrow) int[100];
    return 0;
}

clang 3.0 does not optimize that out anymore, but later versions do.

EDIT2

#include <new>  

int main()
{
    int* mem = new (std::nothrow) int[1000];

    if (mem != 0)
      return 1;

    return 0;
}

clang always returns 1.

请记住,C++ 标准告诉的是正确的程序应该做什么,而不是它应该如何做。它根本不能告诉后来者,因为新的体系结构可以并且确实在编写标准之后出现并且标准必须对它们有用。

new 不必是幕后的系统调用。有些计算机可以在没有操作系统和系统调用概念的情况下使用。

因此,只要最终行为没有改变,编译器就可以优化掉所有的东西。包括 new

有一个警告。
可以在不同的翻译单元中定义替换全局运算符 new
在那种情况下, new 的副作用可能无法优化。但是如果编译器可以保证new运算符没有副作用,就像贴出的代码是整个代码一样,那么优化是有效的。
new can throw std::bad_alloc 不是必需的。这样的话,在优化new的时候,编译器可以保证不会抛出异常,不会产生副作用。

您的代码段中可能发生的最糟糕情况是 new 抛出 std::bad_alloc,这是未处理的。然后会发生什么是实现定义的。

最好的情况是空操作,最坏的情况没有定义,允许编译器将它们分解为不存在的因素。现在,如果您真的尝试捕获可能的异常:

int main() try {
    int* mem = new int[100];
    return 0;
} catch(...) {
  return 1;
}

... 然后 the call to operator new is kept.

完全允许(但不需要)编译器优化原始示例中的分配,在 EDIT1 示例中更是如此,每个§1.9标准,通常称为 as-if 规则:

Conforming implementations are required to emulate (only) the observable behavior of the abstract machine as explained below:
[3 pages of conditions]

cppreference.com.

提供了更易读的表示

相关要点是:

  • 您没有挥发物,因此 1) 和 2) 不适用。
  • 您没有output/write任何数据或提示用户,所以3)和4)不适用。但即使你这样做了,他们显然也会在 EDIT1 中得到满足(在原始示例中可以说 also,尽管从纯理论的角度来看,这是非法的,因为程序流程和输出 - - 理论上 - 不同,但请参阅下面的两段)。

异常,即使是未捕获的异常,也是明确定义的(不是未定义的!)行为。然而,严格来说,如果 new 抛出(不会发生,另见下一段),可观察到的行为会有所不同,无论是程序的退出代码还是程序后面可能跟进的任何输出.

现在,在单个小分配的特定情况下,您可以为编译器提供 "benefit of doubt",它可以 保证 分配不会失败。
即使在内存压力非常大的系统上,当您的可用分配粒度小于可用的最小分配粒度时,甚至无法启动进程,并且堆也会在调用 main 之前设置。因此,如果此分配失败,程序将永远不会启动,或者甚至在 main 被调用之前就已经遇到了不优雅的结局。
到目前为止,假设编译器知道这一点,即使分配 理论上可能会抛出 ,甚至优化原始示例也是合法的,因为编译器实际上可以 保证不会发生。

<有点犹豫>
另一方面,在您的 EDIT2 示例中优化分配是 not 允许的(您可以观察到,这是一个编译器错误)。该值被消耗以产生外部可观察的效果(return 代码)。
请注意,如果您将 new (std::nothrow) int[1000] 替换为 new (std::nothrow) int[1024*1024*1024*1024ll](这是一个 4TiB 分配!),这在当今的计算机上肯定会失败,但它仍然会优化调用。换句话说,它 returns 1 虽然你写的代码必须输出 0.

@Yakk 对此提出了一个很好的论据:只要从未触及内存,就可以 returned 指针,而不需要实际的 RAM。到目前为止,优化 EDIT2 中的分配甚至是合法的。我不确定谁对谁错。

在没有至少两位数 GB RAM 的机器上进行 4TiB 分配几乎肯定会失败,因为 OS 需要创建页表。现在当然,C++ 标准不关心页表或 OS 正在做什么来提供内存,这是真的。

但另一方面,假设 "this will work if memory is not touched" 确实依赖 正是这样的细节和 OS 提供的东西。如果未触及的 RAM 实际上不需要它的假设只是正确的 因为 OS 提供虚拟内存。这意味着 OS 需要创建页表(我可以假装我不知道,但这不会改变我依赖它的事实)。

因此,我认为先假设一个然后说"but we don't care about the other"不是100%正确的。

所以,是的,编译器 可以 假设只要不触及内存,4TiB 分配通常是完全可能的,并且它 可以假设一般情况下是有可能成功的。它甚至可能假设它很可能会成功(即使它不会)。但我认为,无论如何,当存在失败的可能性时,你永远不能假设某些东西 必须 起作用。而且不仅有失败的可能性,在那个例子中,失败甚至是更有可能的可能性。

这是 N3664 允许的。

An implementation is allowed to omit a call to a replaceable global allocation function (18.6.1.1, 18.6.1.2). When it does so, the storage is instead provided by the implementation or provided by extending the allocation of another new-expression.

此提案是 C++14 标准的一部分,因此在 C++14 中,编译器 允许优化 new 表达式(即使它可能会抛出)。

如果您看一下 Clang implementation status,它清楚地表明他们确实实施了 N3664。

如果您在使用 C++11 或 C++03 进行编译时观察到此行为,您应该填写一个错误。

请注意,在 C++14 之前,动态内存分配 是程序可观察状态 的一部分(尽管我目前找不到相关参考),所以在这种情况下,不允许一致的实现应用 as-if 规则。

历史似乎是 clang 遵循 N3664: Clarifying Memory Allocation which allows the compiler to optimize around memory allocations but as Nick Lewycky points out 中规定的规则:

Shafik pointed out that seems to violate causality but N3664 started life as N3433, and I'm pretty sure we wrote the optimization first and wrote the paper afterwards anyway.

所以 clang 实现了优化,后来成为一个提案,作为 C++14 的一部分实现。

基本问题是这是否是 N3664 之前的有效优化,这是一个棘手的问题。我们将不得不转到 C++ 标准草案部分 1.9 程序执行 中涵盖的 as-if rule ,其中说( 强调我的 ):

The semantic descriptions in this International Standard define a parameterized nondeterministic abstract machine. This International Standard places no requirement on the structure of conforming implementations. In particular, they need not copy or emulate the structure of the abstract machine. Rather, conforming implementations are required to emulate (only) the observable behavior of the abstract machine as explained below.5

注释 5 说:

This provision is sometimes called the “as-if” rule, because an implementation is free to disregard any requirement of this International Standard as long as the result is as if the requirement had been obeyed, as far as can be determined from the observable behavior of the program. For instance, an actual implementation need not evaluate part of an expression if it can deduce that its value is not used and that no side effects affecting the observable behavior of the program are produced.

由于 new 可能会抛出一个异常,该异常会改变程序的 return 值,因此会出现可观察到的行为,这似乎反对 如同规则

虽然,可以说它是实现细节,何时抛出异常,因此即使在这种情况下,clang 也可以决定它不会导致异常,因此省略 new 调用不会违反 如同规则

as-if 规则 下似乎也可以有效地优化对非抛出版本的调用。

但是我们可以在不同的翻译单元中使用替换的全局运算符 new,这可能会影响可观察到的行为,因此编译器必须有某种方式来证明情况并非如此,否则就不会能够在不违反 as-if 规则 的情况下执行此优化。以前版本的 clang 确实在这种情况下优化为 this godbolt example shows which was provided via Casey here,取此代码:

#include <cstddef>

extern void* operator new(std::size_t n);

template<typename T>
T* create() { return new T(); }

int main() {
    auto result = 0;
    for (auto i = 0; i < 1000000; ++i) {
        result += (create<int>() != nullptr);
    }

    return result;
}

并将其优化为:

main:                                   # @main
    movl    00000, %eax          # imm = 0xF4240
    ret

这确实看起来太激进了,但后来的版本似乎没有这样做。