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


72

考虑使用以下简单代码new(我知道没有delete[],但是与这个问题无关):

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

    return 0;
}

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

在我的研究中,g ++(5.2.0)和Visual Studio 2015不能优化new调用,而clang(3.0+)可以优化调用。所有测试均已启用了全部优化功能(对于g ++和clang为-O3,对于Visual Studio为Release模式)。

是不是 new幕后进行系统调用,从而使编译器无法(而且是非法的)优化它吗?

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

#include <new>  

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

clang 3.0不再优化了,但是更高版本

编辑2

#include <new>  

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

    if (mem != 0)
      return 1;

    return 0;
}

clang总是返回1

Answers:


59

历史似乎是clang遵循N3664列出的规则:澄清内存分配,这使编译器可以围绕内存分配进行优化,但正如Nick Lewycky指出的那样

Shafik指出这似乎违反了因果关系,但N3664最初是N3433,我很确定我们先写了优化程序,然后写了论文。

因此clang实现了优化,后来成为了C ++ 14的一部分。

基本问题是,这是否是之前的有效优化N3664,这是一个难题。我们将不得不去看C ++标准草案程序执行部分中包含的假设规则,该规则说(强调我的):1.9

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

注释5说:

该规定有时被称为“假设”规则,因为只要可以根据可观察到的行为确定结果,就可以无视本国际标准的任何要求,而该实现可以自由地执行。该程序。例如,如果实际实现可以推断出未使用其值并且不会产生影响程序可观察行为的副作用,则无需评估表达式的一部分。

由于new可能会抛出一个异常,该异常具有可观察的行为,因为它将更改程序的返回值,因此似乎反对了as-if规则允许它。

尽管可以争论的是何时抛出异常是实现细节,因此clang即使在这种情况下也可以决定它不会导致异常,因此取消new调用不会违反as-if规则

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

但是我们可以在不同的翻译单元中使用新的替换全局运算符,这可能会导致它影响可观察到的行为,因此编译器将不得不采取某种方式证明情况并非如此,否则它将无法执行此优化而不违反常规规则。在这种情况下,以前的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

这确实看起来过于激进,但是更高版本似乎没有这样做。


12
像这样的答案是使StackOverflow成为无价之宝的原因。太棒了
巴雷特·阿黛尔

20

N3664允许这样

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

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

如果查看Clang的实现状态,它会清楚地表明它们确实实现了N3664。

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

请注意,在C ++ 14之前,动态内存分配程序可观察状态一部分(尽管目前我无法找到该引用),因此在此情况下,不允许一致的实现应用按条件规则案件。


@Banex IMH是的。基本上允许实现用自动存储代替动态存储。由于分配自动存储不会失败, mem != nullptr因此始终如此。您应该提及所使用的标准版本。
sbabbi 2015年

我懂了。没错,Clang 3.4+符合标准。但是,根据其状态页未实现N3664的Clang 3.3也会优化此类代码。因此,至少该版本存在错误。
Banex

1
@Banex那个提议是由c族人提出的。我相信发生的事情是他们首先实现了(非平凡的)优化过程,后来又发现它不符合标准……并提出了解决方案。
sbabbi 2015年

7
该N3664提案被称为“澄清内存分配”。目的不是更改标准,而是明确允许某些优化。在示例中,它将“ new-expression通过调用分配函数(3.7.4.1)获取对象的存储”更改为“ new-expression可通过调用分配函数(3.7.4.1)获取对象的存储”。我认为,“按条件”条款下“可能获得”已经成为可能。N3664只是使它明确。因此,我认为符合3.3。
匿名Co夫

9

请记住,C ++标准告诉您正确的程序应该做什么,而不是程序应该如何做。它根本无法告诉后面的人,因为新架构可以而且确实会在编写标准之后出现,并且必须对其使用标准。

new不必在后台进行系统调用。有些可用的计算机没有操作系统,也没有系统调用的概念。

因此,只要最终行为不变,编译器就可以优化所有内容。包括那个new

有一个警告。
可以在不同的翻译单元中定义替代全局运算符new。
在这种情况下,new的副作用可能无法被优化。但是,如果编译器可以保证new运算符没有副作用,例如如果发布的代码是完整代码,则该优化是有效的。
那个新的可以抛出std :: bad_alloc不是必需的。在这种情况下,当对new进行优化时,编译器可以保证不会引发任何异常并且不会发生副作用。


4
请记住,C ++标准告诉您正确的程序应该做什么,而不是正确的程序。掩盖了一些细节,它们对这个问题很重要。请参阅我上面链接的可能重复项。
沙菲克·雅格慕

1
我已经检查了一下,它巩固了我的位置。仅要求编译器生成执行“按原样”的代码。唯一有意义的部分是“可以在不同的翻译单元中定义新的替换全局运算符”
Anonymous Coward 2015年

1
@JoseAntonioDuraOlmos这里的问题是“堆是可观察状态的一部分吗?” 如果答案为“是”,则“视情况”规则不适用。
sbabbi 2015年

2
未分配的堆不是可观察状态的一部分。除其他事项外,因为具有随时间变化的大小的堆是可以接受的。优化分配只会对未分配的堆产生影响(它将比未优化分配的情况大)。它对已经分配的空间没有影响,那些是可观察的。
匿名Co夫

1
我敢冒险说该程序根本没有可观察到的效果(没有volatile对不透明函数的访问或调用)。堆本身是不可观察的。
西蒙·里希特

7

对于编译器来说,完全可以允许(但不是必需)在您的原始示例中优化分配,甚至在按照标准§1.9的EDIT1示例中更是如此(通常称为as-if规则)

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

cppreference.com上可以找到更易于理解的表示形式。

相关要点是:

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

异常,甚至是未捕获的异常,都是定义明确(并非未定义!)的行为。但是,严格来说,如果发生new抛出(不会发生,也请参见下一段),则可观察到的行为将有所不同,无论是程序的退出代码还是程序后面可能出现的任何输出。

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

<稍微不确定>
另一方面,不允许在EDIT2示例中优化分配(如您所见,是编译器错误)。消耗该值以产生外部可观察的效果(返回码)。
请注意,如果您将替换new (std::nothrow) int[1000]new (std::nothrow) int[1024*1024*1024*1024ll](这是4TiB分配!),这在当前的计算机上肯定会失败,那么它仍然可以优化呼叫。换句话说,尽管您编写了必须输出0的代码,但它返回1。

@Yakk对此提出了一个很好的论据:只要不触摸内存,就可以返回指针,并且不需要实际的RAM。就此而言,优化EDIT2中的分配甚至是合法的。我不确定谁是对的,谁是错的。

仅仅因为操作系统需要创建页表,在没有至少2位GB的RAM的机器上进行4TiB分配几乎可以保证失败。当然,现在,C ++标准并不关心页表或操作系统正在做什么以提供内存,这是事实。

但是另一方面,“如果不触摸内存,这将起作用”这一假设确实依赖于这样的细节以及操作系统提供的内容。如果没有被碰过它实际上是不需要RAM是唯一真正的假设,因为操作系统提供的虚拟内存。这意味着操作系统需要创建页表(我可以装作自己不知道它,但是这并不能改变我仍然依赖它的事实)。

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

因此,是的,只要不触摸内存,编译器就可以假定通常完全可以实现4TiB分配,并且可以假定通常可以成功。它甚至可能假设它可能成功(即使不是成功)。但是我认为,无论如何,在发生故障的可能性时,您永远都不能假定必须采取某些措施。而且不仅存在故障的可能性,在该示例中,故障甚至更有可能发生。
</略有未定>


2
我认为此答案需要引用为什么new要进行4 TiB分配。

3
我不同意:编译器可以自由返回1。在未使用内存的情况下,未分配的内存的行为与就标准而言分配的内存完全一样。 new可以返回一个带有非空值的指针,该值不指向任何内容,并且如果编译器可以证明未对指向的对象进行定义的访问,则它可以满足标准的要求。如果delete可以调用,事情会变得更加棘手,但只会
有些微

2
@damon C ++标准没有描述页面描述符:它们的状态是实现细节,因此在as-if下无关紧要。
Yakk-Adam Nevraumont

3
是的,这是合法的,您一直在谈论不相关的实现细节:如果不关心它将如何实现。不,不需要编译器进行优化:编译器可以自由地始终对进行每次调用new,但这样做是实现质量的问题。可以“诚实地”完成尝试分配4个attobytes的操作,并且可以throw不加尝试地将其变为a ,或者如果证明从未使用过,则可以将其变为noop。分配1个字节相同(除了诚实分支更有可能工作)
Yakk-Adam Nevraumont

2
@Damon:如果我编写int foo(unsigned long long n) { unsigned long long a,b; a=0; for (b=0; b<n; b++) a++; return a; }标准中的任何内容,将禁止编译器将其替换为{ return n; }?如果编译器可以弄清楚一台机器如果有足够的时间和足够的内存会做什么,则不需要它实际使用该时间或内存。
2015年

2

代码段中最糟糕的情况是newthrows std::bad_alloc,这是未处理的。然后发生的事情是实现定义的。

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

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

...然后保持通话operator new


它保存在该编译器中。但是,针对您的答案中的特定代码对其进行优化是否符合标准?我认同。
匿名Co夫

@JoseAntonioDuraOlmos如果将更100改为某个较大的值,则可能会期望分配失败,并且优化new分离将意味着更改程序的可观察行为。编译器也不能总是失败,因为同一程序将来可能在具有3艾字节内存的计算机上运行,​​并且有望成功。
昆汀

@ Jarod42这个人很好奇,成功和失败都会导致无操作,但它并没有得到优化。但是要弄清楚为什么编译器保留代码比为什么要丢弃代码要困难得多。编辑:OP对其进行了整理:更高版本将其删除。
昆丁

@JoseAntonioDuraOlmos,现在我用Clang 3.6尝试了它……实际上它总是返回零。那是一个错误。
2015年

2
@quen分配失败时是实现定义的。作为一个成功的分配没有副作用超出返回0,该程序返回0表现为-如果分配succeedes,因此是一个符合程序与分配成功(即使它在attobytes测量)。分配失败仅仅是实现问题。(请注意,每个分配失败的程序都符合要求)
Yakk-Adam Nevraumont
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.