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

Ban*_*nex 66 c++ optimization gcc clang language-lawyer

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

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

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

是否允许编译器优化new呼叫?

在我的研究中,g ++(5.2.0)和Visual Studio 2015不会优化new呼叫,而clang(3.0+)则可以.所有测试都是在启用完全优化的情况下进行的(-O3用于g ++和clang,用于Visual Studio的发布模式).

是不是new在引擎盖下进行系统调用,使编译器无法(并且非法)优化它?

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

#include <new>  

int main()
{
    int* mem = new (std::nothrow) int[100];
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

clang 3.0不再优化它,但后来的版本确实如此.

编辑2:

#include <new>  

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

    if (mem != 0)
      return 1;

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

clang总是返回1.

Sha*_*our 52

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

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

所以clang实现了优化,后来成为了作为C++ 14的一部分实现的提议.

基本问题是这是否是一个有效的优化之前N3664,这是一个棘手的问题.我们必须转到C++标准部分程序执行中所述的as-if规则,该1.9 程序执行说(强调我的):

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

注意5说:

这项规定有时被称为"假设"规则,因为只要结果就像是遵守了要求,只要可以从可观察的行为中确定,实施就可以自由地忽视本国际标准的任何要求.该计划.例如,实际实现不需要评估表达式的一部分,如果它可以推断出它的值没有被使用,并且没有产生影响程序的可观察行为的副作用.

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

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

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

但是我们可以在不同的转换单元中使用替换全局运算符new,这可能会导致这会影响可观察行为,因此编译器必须有某种方式证明这不是这种情况,否则它将无法执行此优化不违反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;
}
Run Code Online (Sandbox Code Playgroud)

并优化它:

main:                                   # @main
    movl    $1000000, %eax          # imm = 0xF4240
    ret
Run Code Online (Sandbox Code Playgroud)

这确实看起来过于激进,但后来的版本似乎没有做到这一点.

  • 像这样的答案是使StackOverflow成为无价之宝的原因.布拉沃. (10认同)

sba*_*bbi 19

这是N3664允许的.

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

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

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

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

请注意,在C++ 14之前,动态内存分配程序可观察状态一部分(虽然我目前找不到该引用),因此不允许在此处应用as-if规则.案件.

  • N3664提案被称为"澄清内存分配".目的不是改变标准,而是明确允许某些优化.在示例中,它通过调用分配函数(3.7.4.1)将"new-expression获取对象的存储"更改为"new-expression可以通过调用分配函数(3.7.4.1)获取对象的存储".我认为"可以获得"在"as-if"条款下已经可以实现.N3664只是明确了.因此我认为3.3符合. (7认同)
  • @Banex 这个提案是由 clang 的人提出的。我相信发生的事情是他们首先实现了(不平凡的)优化过程,后来发现它不符合标准......并填写了一份提案来解决这个问题。 (2认同)

Jos*_*ica 9

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

new不必是引擎盖下的系统调用.有些计算机可以在没有操作系统且没有系统调用概念的情况下使用.

因此,只要结束行为没有改变,编译器就可以优化任何和所有内容.包括那个new

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

  • *请记住,C++标准告诉了正确的程序应该做什么,而不是它应该如何做.*有点掩盖一些细节,它们对这个问题很重要.请参阅上面链接的可能重复内容. (4认同)
  • 未分配的堆不是可观察状态的一部分.除此之外,因为可以接受具有随时间变化的大小的堆.优化掉分配只会对未分配的堆产生影响(它会比分配未优化时更大).它对已经分配的空间没有影响,那些是可观察的空间. (2认同)

Dam*_*mon 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中优化分配甚至是合理的.我不确定谁是对的,谁在这里错了.

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

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

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

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

  • 我不同意:编译器可以自由返回1.在未使用内存的情况下,未分配的内存的行为完全如同 - 如果它被分配到标准所涉及的那样.`new`可以返回一个指向空的非空值的指针,如果编译器可以证明没有对指向的内容进行定义访问,它就会传递标准的要求.如果可以调用`delete`,事情变得棘手,但只是轻微(类似的参数也可以跳过该调用) (3认同)
  • 是的,这是合法的,你继续谈论不相关的实现细节:as-if不关心它将如何实现.不,编译器不需要进行优化:编译器可以随时抛出对"new"的每次调用,而不是这样做是一个实现质量问题.尝试分配4个attobytes可以"诚实地"完成并投掷,在不尝试的情况下变成'throw`,或者如果可以证明从未使用变成noop.分配1个字节相同(除了更可能工作的诚实分支) (3认同)
  • 我认为这个答案需要引述为什么要要求“ new”来分配4 TiB分配。 (2认同)
  • @damon C++标准没有描述页面描述符:它们的状态是一个实现细节,因此在as-if下无关紧要. (2认同)
  • @Damon:如果我写`int foo(unsigned long long n){unsigned long long a,b; 一个= 0; for(b = 0; b <n; b ++)a ++; 返回; 标准中的任何内容都禁止编译器用`{return n; }`?如果编译器能够弄清楚如果它有足够的时间和足够的内存,机器会做什么,那么就没有必要让它实际使用那个时间或内存. (2认同)