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

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

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

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

在我的研究中, g ++(5.2.0)和Visual Studio 2015没有优化new调用, 而铿锵(3.0+) 。 所有的testing都是在启用完全优化的情况下完成的(-O3代表g ++和clang,Visual Studio的发布模式)。

是不是new进行下的系统调用,使编译器不可能(和非法)优化呢?

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

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

铿锵3.0不会优化出来 ,但后来的版本 。

编辑2

 #include <new> int main() { int* mem = new (std::nothrow) int[1000]; if (mem != 0) return 1; return 0; } 

铿锵总是回报1 。

历史似乎是clang遵循N3664规定的规则:澄清内存分配 ,允许编译器优化内存分配,但正如Nick Lewycky所指出的那样 :

Shafik指出,似乎违反因果关系,但N3664开始作为N3433生活,我敢肯定,我们先写优化,然后写论文。

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

基本的问题是这是否是N3664之前的有效优化,这是一个棘手的问题。 我们将不得不按照C ++标准草案1.9程序执行中所述的(如我的重点 )的规定进行:

本标准中的语义描述定义了一个参数化的非确定性抽象机器。 本国际标准对合规实施的结构没有要求。 特别是,他们不需要复制或模拟抽象机器的结构。 相反, 需要符合的实现来模拟(仅)抽象机器的可观察行为,如下所述。

5说:

这个规定有时被称为“现在如果”的规则 ,因为只要结果是仿效了这个要求,就可以从可观察到的行为中确定,实现就可以自由地忽视这个国际标准的任何要求的程序。 例如,一个实际的实现不需要评估一个expression式的一部分,如果它能够推断出它的值没有被使用,并且没有产生影响该程序的可观察行为的副作用。

因为new可能会抛出一个exception,因为它会改变程序的返回值,所以会有可观察到的行为,这似乎是反对它被允许的如果如果规则

虽然,可能会认为它是实现细节什么时候抛出一个exception,因此铛可以决定,即使在这种情况下,它不会导致一个exception,因此,消除new调用不会违反as-if规则

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

但是我们可以在不同的翻译单元中有一个新的replace全局操作符,这可能会影响可观察到的行为,所以编译器必须有某种方式certificate事实并非如此,否则将无法执行这种优化而不违反“ 如果”的规定 。 以前的版本铛确实优化在这种情况下,因为这个godbolt示例显示哪些是通过凯西在这里提供,采取此代码:

 #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 

这确实看起来太积极,但后来的版本似乎并没有这样做。

这是N3664允许的。

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

这个build议是C ++ 14标准的一部分,所以在C ++ 14中,编译器允许优化一个newexpression式(即使它可能抛出)。

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

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

请注意,在C ++之前,14个dynamic内存分配程序可观察状态一部分 (尽pipe目前我还找不到这个参考),所以一致的实现不允许在这里应用as-if规则案件。

记住C ++标准告诉程序应该做什么,而不是应该怎么做。 因为新的架构在标准制定之后就可以出现,标准必须对他们有用,所以它不能告诉后来者。

new不一定是引擎盖下的系统调用。 没有操作系统,没有系统调用的概念,有计算机可用。

因此,只要最终的行为不改变,编译器就可以优化任何事情。 包括那个new

有一个警告。
一个替代的全球运营商新可能已被定义在一个不同的翻译单位
在这种情况下,新的副作用可能是无法优化的。 但是,如果编译器可以保证新的操作符没有副作用,就像发布的代码是整个代码那样,那么优化是有效的。
新的可以抛出std :: bad_alloc是不是一个要求。 在这种情况下,当新的优化时,编译器可以保证不会抛出exception,不会产生副作用。

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

为了模拟(仅)抽象机器的可观察行为,需要遵循相应的实现,如下所述:
[3页的条件]

cppreference.com上提供了一个更易读的表示forms。

相关的观点是:

  • 你没有挥发性物质,所以1)和2)不适用。
  • 您不输出/写入任何数据或提示用户,所以3)和4)不适用。 但即使你这样做了,他们在EDIT1中显然也会满意(可以说也是在原来的例子中,尽pipe从纯粹的理论angular度来看,这是非法的,因为程序stream程和输出在理论上是不同的,下面)。

一个例外,即使是一个未被捕获的,也是非常明确的(而不是未定义的)行为。 然而,严格来说,如果new投掷(不会发生,另见下一段),可观察到的行为将会不同,无论是程序的退出代码还是程序后面的任何输出。

现在,在一个单独的小configuration的特定情况下,您可以给编译器“可疑的好处” ,它可以保证分配不会失败。
即使在内存压力很大的系统上,如果你的系统的最小分配粒度小于可用的最小分配粒度,也不可能启动一个进程,并且在调用main之前,堆已经被build立了。 所以,如果这个分配失败了,那么这个程序就不会启动,或者在main被调用之前就已经遇到了一个不正常的结束。
假设编译器知道这一点,即使分配理论上可以抛出 ,甚至优化原来的例子也是合法的,因为编译器可以保证它不会发生。

<稍微未定>
另一方面,这是不允许的 (正如你可以观察到的,一个编译器错误)来优化你的EDIT2例子中的分配。 该值用于产生一个外部可观察的效果(返回码)。
请注意,如果你用new (std::nothrow) int[1000] new (std::nothrow) int[1024*1024*1024*1024ll] (这是一个4TiB分配!)replacenew (std::nothrow) int[1000] – 保证失败,它仍然优化了通话。 换句话说,它返回1,尽pipe你写的代码必须输出0。

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

做一个4TiB分配几乎可以保证在一台机器上失败,因为操作系统需要创build页表,所以至less不会有两位数的GB内存。 当然,C ++标准并不关心页表或者操作系统在提供内存方面做的事情。

但是另一方面,假设“如果内存没有被触及,这个工作就可以运行”,这个细节和操作系统所提供的内容完全依赖于这个细节。 假设如果没有被触摸的RAM实际上不需要,那么这只是因为操作系统提供了虚拟内存。 这意味着操作系统需要创build页面表(我可以假装我不知道这个,但是这并不改变我依靠它的事实)。

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

所以,是的,编译器可以假设一个4TiB的分配通常是完全可能的,只要内存不被触及,就可以假定一般情况下可以成功。 甚至可能认为它可能成功(即使不是这样)。 但是我认为,无论如何,在发生失败的可能性时,你绝对不能认为有什么必须工作。 不仅有失败的可能性,在这个例子中,失败甚至更有可能
</稍微未定>

在你的代码片段中可能发生的最糟糕的是new std::bad_alloc ,它是未处理的。 接下来会发生什么是实现定义的。

最好的情况是无操作,最坏的情况是没有定义,编译器可以将它们分解成不存在的。 现在,如果你真的尝试了解可能的例外:

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

…然后呼叫operator new保持 。