编译器是否允许优化堆内存分配?
考虑以下使用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中,编译器允许优化一个new
expression式(即使它可能抛出)。
如果你看一下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
保持 。