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

时间:2015-08-07 08:57:25

标签: c++ gcc optimization clang language-lawyer

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

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

    return 0;
}

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

在我的研究中,g++ (5.2.0)和Visual Studio 2015不会优化new来电,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了,但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

5 个答案:

答案 0 :(得分:45)

历史似乎是clang遵循N3664: Clarifying Memory Allocation中规定的规则,允许编译器优化内存分配,但是Nick Lewycky points out

  

Shafik指出,似乎违反了因果关系,但N3664的起点是N3433,我很确定我们先写了优化,然后再写论文。

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

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

  

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

注意5说:

  

此规定有时称为“as-if”规则,因为   实施可以自由地忽视任何要求   国际标准只要结果就好像要求一样   从可观察的角度来看,已经服从了   该计划的行为。例如,实际的实施需要   如果它可以推导出它的值,则不评估表达式的一部分   未使用,没有影响可观察行为的副作用   制作该节目。

由于new可以抛出一个具有可观察行为的异常,因为它会改变程序的返回值,这似乎反对 as-if规则允许

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

as-if规则下似乎也有效,以优化对非投掷版本的调用。

但是我们可以在不同的翻译单元中使用替换全局运算符new,这可能会导致这会影响可观察行为,因此编译器必须有某种方式证明这不是这种情况,否则它将无法在不违反 as-if规则的情况下执行此优化。以前版本的clang确实在这种情况下优化为this godbolt example shows,它是通过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    $1000000, %eax          # imm = 0xF4240
    ret

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

答案 1 :(得分:18)

N3664允许这样做。

  

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

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

如果您查看Clang implementation status,请明确说明他们确实实施了N3664。

如果在C ++ 11或C ++ 03中编译时发现此行为,则应填写错误。

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

答案 2 :(得分:10)

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

new不一定是一个系统调用。有些计算机可以在没有操作系统的情况下使用,也没有系统调用的概念。

因此,只要结束行为没有改变,编译器就可以优化任何一切。包括public class MyException extends Exception { public MyException (Exception ex) { super(ex); } public MyException (String message) { super(message); } public MyException (Exception ex,String moduleKey) { super(ex, moduleKey); } public MyException (Exception ex, String moduleKey, String message) { super(ex, moduleKey, message); }

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

答案 3 :(得分:7)

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

  

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

cppreference.com提供了一种更易于阅读的表示形式。

相关要点是:

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

一个例外,即使是未被捕获的例外,也是明确定义的(未定义!)行为。但是,严格来说,如果new抛出(不会发生,请参见下一段),可观察的行为将会有所不同,包括程序的退出代码和程序后面可能跟随的任何输出

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

&lt;稍微犹豫不决&gt;
另一方面,它是允许(并且你可以观察到,编译器错误)来优化你的EDIT2示例中的分配。消耗该值以产生外部可观察的效果(返回码) 请注意,如果将new (std::nothrow) int[1000]替换为new (std::nothrow) int[1024*1024*1024*1024ll](这是4TiB分配!),即 - 在当前计算机上 - 保证失败,它仍会优化呼叫。换句话说,尽管您编写了必须输出0的代码,但它返回1。

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

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

但另一方面,假设“如果没有触及内存,这将有效”确实依赖确切地说明这样的细节以及操作系统提供的内容。假设如果实际上不需要未触及它的RAM,则仅为真,因为 OS提供虚拟内存。这意味着操作系统需要创建页面表(我可以假装我不知道它,但这并不会改变我依赖它的事实。)

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

所以,是的,编译器可以假设只要没有触及内存就可以完全实现4TiB分配,并且它可以假设它通常是有可能成功。它甚至可能认为它可能会成功(即使它没有)。但我认为,无论如何,当有可能出现故障时,你永远不会认为必须有效。并且不仅存在失败的可能性,在该示例中,失败甚至是更可能的可能性。
&lt; /略有未定&gt;

答案 4 :(得分:2)

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

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

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

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