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

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

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


return 0;
}

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

在我的研究中,G + + (5.2.0)和 VisualStudio2015并没有优化 new调用 而 clang (3.0 +)可以。所有测试都启用了完全优化(- O3 for g + + and clang,Release mode for Visual Studio)。

难道 new不是在底层进行系统调用,使得编译器不可能(也是非法的)优化它吗?

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

#include <new>


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

Clang 3.0不再优化 ,而是优化 后来的版本可以

EDIT2 :

#include <new>


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


if (mem != 0)
return 1;


return 0;
}

Clang 总是返回1

7391 次浏览

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

new不必是一个隐藏在引擎盖下的系统调用。有些计算机可以在没有操作系统和没有系统调用概念的情况下使用。

因此,只要结束行为没有改变,编译器就可以优化任何东西。包括那个 new

有一点需要注意。
可以在不同的翻译单元中定义替换全局运算符 new
在这种情况下,新的副作用可能是这样的,不能被优化消除。但是如果编译器可以保证 new 操作符没有副作用,就像发布的代码是整个代码一样,那么优化是有效的。
这个新的可以抛出 std: : bad _ alloc 并不是必需的。在这种情况下,当 new 被优化时,编译器可以保证不会引发异常,也不会发生副作用。

在代码段中最坏的情况是 new抛出未处理的 std::bad_alloc。然后发生的是实现定义的。

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

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

然后是 打给 operator new的电话保持不变

编译器完全允许(但是 不需要)优化原始示例中的分配,在每1.9个标准的 EDIT1示例中更是如此,这通常被称为 仿佛统治一切:

符合要求的实现(仅)需要模拟抽象机器的可观察行为,如下所述:
[3页条件]

Cppreference.com提供了更易于阅读的表示。

相关要点如下:

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

一个异常,即使是未捕获的异常,也是定义良好的(而不是未定义的!)行为。然而,严格地说,如果 new抛出(不会发生,请参阅下一段) ,可观察到的行为将是不同的,无论是由程序的退出代码和任何输出可能随后在程序中。

现在,在单数小分配的特殊情况下,可以给编译器 “无罪推定”,它可以 保证,这样分配就不会失败。
即使在内存压力非常大的系统上,如果可用的分配粒度小于最小值,甚至不可能启动一个进程,而且在调用 main之前堆也已经设置好了。因此,如果这个分配失败,程序将永远不会启动,或者在调用 main之前就已经遇到了不体面的结局。
到目前为止,假设编译器知道这一点,即使分配 理论上来说,优化原始示例也是合法的,因为编译器可以 差不多吧保证不会发生这种情况。

< 有点犹豫不决 >
另一方面,在您的 EDIT2示例中,没有允许优化分配(正如您可以观察到的,编译器错误)。该值用于产生外部可观察到的效果(返回代码)。
注意,如果用 new (std::nothrow) int[1024*1024*1024*1024ll]替换 new (std::nothrow) int[1000](这是一个4TiB 分配!)在今天的计算机上,这是注定要失败的,它仍然优化了调用。换句话说,它返回1,尽管您编写的代码必须输出0。

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

在一台至少没有两位数千兆内存的机器上进行4TiB 分配几乎肯定会失败,因为操作系统需要创建页表。当然,C + + 标准并不关心页表或操作系统如何提供内存,这是事实。

但另一方面,假设“如果不触及内存,这将工作”的 依赖正是这样的细节和操作系统提供的东西。如果没有接触到 RAM,那么它实际上不需要的假设只是真实的 因为操作系统提供了虚拟内存。这意味着操作系统需要创建页表(我可以假装不知道,但这并不能改变我依赖它的事实)。

因此,我认为先假设一个,然后说“但是我们不关心另一个”是不100% 正确的。

所以,是的,编译器 可以假设只要不触及内存,4TiB 分配通常是完全可能的,而且它假设通常是可能成功的。它甚至可能认为它有可能成功(即使它不是)。但是我认为在任何情况下,您都不能假设 必须的工作时有一个失败的可能性。不仅存在失败的可能性,在这个例子中,失败甚至是 更有可能的可能性。
尚未决定

这是 N3664允许的。

允许实现省略对可替换全局分配函数的调用(18.6.1.1,18.6.1.2)。当它这样做时,存储将由实现提供,或者通过扩展另一个新表达式的分配来提供。

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

如果你看一下 Clang 实现状态,它清楚地表明他们确实实现了 N3664。

如果您在编译 C + + 11或 C + + 03时观察到这种行为,那么您应该填补一个 bug。

请注意,在 C + + 14之前,动态内存分配程序的 是可见状态的一部分(尽管我现在找不到它的引用) ,因此在这种情况下,不允许一致的实现应用 好像规则。

历史似乎是叮当声遵循 N3664: 澄清内存分配中的规则,它允许编译器围绕内存分配进行优化,但是作为 Nick Lewycky 指出:

Shafik 指出这似乎违反了因果关系,但是 N3664以 N3433开始生命,我很确定我们先写了优化,然后再写论文。

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

基本问题是这是否是 N3664之前的一个有效的优化,这是一个棘手的问题。我们将不得不去 仿佛统治一切涵盖的草案 C + + 标准部分 1.9 程序执行说(强调我的) :

本国际标准中的语义描述定义了 参数化不确定抽象机 标准对合格的结构没有要求 特别是,它们不需要复制或模拟 抽象机器的结构。相反,< strong > 一致的实现 要求(仅仅)模仿抽象的可观察的行为 机器 如下所解释

其中 5注明:

此规定有时称为 “似是而非”原则,因为 实现可以自由地忽略这方面的任何要求 只要达到国际标准的结果就好像是要求的一样 已经被遵守,至少从可观察到的 程序的行为。例如,一个实际的实现需要 如果能够推断出某个表达式的值是 不使用,并且没有副作用影响观察到的行为 节目制作完成。

由于 new可能抛出一个异常,这个异常将具有可观察的行为,因为它会改变程序的返回值,这似乎与 仿佛统治一切允许它存在争议。

虽然,可以说抛出异常的时间是实现细节,因此 clang 可以决定即使在这种情况下也不会引起异常,因此省略 new调用不会违反 仿佛统治一切

仿佛统治一切下优化对非抛出版本的调用似乎也是有效的。

但是我们可以在不同的翻译单元中使用一个替换的全局操作符 new,这可能会影响可观察到的行为,因此编译器必须有某种方法来证明这不是事实,否则它将无法在不违反 仿佛统治一切的情况下执行这种优化。在这种情况下,以前版本的 clang 确实优化为 这个 Godbolt 的例子显示,这是通过 我是 Casey提供的,使用以下代码:

#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    $1000000, %eax          # imm = 0xF4240
ret

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