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)
编辑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)
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)
这确实看起来过于激进,但后来的版本似乎没有做到这一点.
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规则.案件.
请记住,C++标准告诉了正确的程序应该做什么,而不是它应该如何做.它根本无法告诉后者,因为在编写标准并且标准必须对它们有用之后,新架构能够并且确实出现了.
new不必是引擎盖下的系统调用.有些计算机可以在没有操作系统且没有系统调用概念的情况下使用.
因此,只要结束行为没有改变,编译器就可以优化任何和所有内容.包括那个new
有一点需要注意.
可以在不同的翻译单元中定义替换全局运算符new.
在这种情况下,new的副作用可能是无法优化的.但是,如果编译器可以保证新运算符没有副作用,如果发布的代码是整个代码就是这种情况,那么优化是有效的.
新的可以抛出std :: bad_alloc不是必需的.在这种情况下,当优化new时,编译器可以保证不会抛出任何异常并且不会发生副作用.
编译器在原始示例中优化分配是完全允许的(但不是必需的),在标准的§1.9的EDIT1示例中更是如此,这通常被称为as-if规则:
符合实现需要模拟(仅)抽象机器的可观察行为,如下所述:
[3页条件]
cppreference.com提供了更易于阅读的表示形式.
相关要点是:
一个例外,即使是未被捕获的例外,也是明确定义的(未定义!)行为.但是,严格地说,如果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分配通常是完全可能的,并且它可以假设通常可以成功.它甚至可能认为它可能会成功(即使它没有).但我认为,无论如何,当有可能出现故障时,你永远不会认为某些事情必须有效.并且不仅存在失败的可能性,在该示例中,甚至更可能失败.
</略有未定>