Clang与GCC-包括operator new的优化

7

我有一个简单的示例进行测试,发现在涉及operator new时,gcc优化(-O3)似乎不如clang。我想知道可能的问题,并且是否可能以某种方式强制gcc生成更多优化的代码?

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;
}


#clang3.6++ -O3 -s --std=c++11 test.cpp
#size a.out
   text    data     bss     dec     hex filename
   1324     616       8    1948     79c a.out
#time ./a.out 
real 0m0.002s
user 0m0.001s
sys  0m0.000s

#gcc4.9 -O3 -s --std=c++11 test.cpp
#size a.out
   text    data     bss     dec     hex filename
   1484     624       8    2116     844 a.out
#time ./a.out
real 0m0.045s
user 0m0.035s
sys  0m0.009s

上面的示例只是我一开始测试代码的简单版本,但它仍然说明了gcc/clang之间的差异。我也检查了汇编代码,在大小上并没有太大的区别,但在性能上肯定有差异。另一方面,也许clang正在做一些不允许的事情?


1
使用 godbolt 作为这个例子的工具,clang 似乎把所有代码优化成了一个 movl $1000000, %eax 操作,而 gcc 则没有。 - Shafik Yaghmour
1
我想这取决于clang正在执行的优化是否符合as-if规则。 - Shafik Yaghmour
直观地说,内存泄漏并不属于“就像”规则的范畴,但这可能是一个需要向标准专家咨询的问题。 - Mark B
@MarkB 是的,但如果 new 抛出异常,那么程序的返回值将会改变,据我所知这属于可观察行为。 - Shafik Yaghmour
1
大多数情况下,gcc将operator new操作符视为常规函数。如果您提供一个调用malloc的内联版本的operator new操作符,那么gcc可能会做一些魔法。 - Marc Glisse
显示剩余2条评论
3个回答

14
如果我们将这段代码插入到godbolt中,我们可以看到clang将代码优化为以下内容:
main:                                   # @main
movl    $1000000, %eax          # imm = 0xF4240
ret

虽然gcc没有执行此优化,但问题是这是否为有效的优化?这是否遵循C++标准草案1.9节“程序执行”中所述的as-if规则,该规则指出(强调我的):

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

其中注释5说:

这条条款有时被称为“as-if”规则,因为只要结果与从程序的可观察行为可以推断出需遵守的要求一样,实现就可以自由地忽略本国际标准的任何要求。例如,如果实现可以推断出表达式的一部分未被使用且不会产生影响程序的可观察行为的副作用,则实际实现不需要计算该部分表达式。

由于new可能会抛出异常,因此会产生可观察行为,因为它会改变程序的返回值。

R.MartinhoFernandes认为什么时候引发异常是实现细节,因此clang可以决定这种情况不会导致异常,因此省略new调用不违反as-if规则。对我来说,这似乎是一个合理的论点。

但正如T.C.所指出的:

替换全局运算符new可以在另一个翻译单元中定义

Casey提供了一个示例,即使clang看到有替换,它仍然执行此优化,即使存在丢失的副作用也是如此。因此,这似乎是过度激进的优化。

请注意,内存泄漏不是未定义行为


2
“new” 可能会抛出异常,但不一定会这样做。最终,决定何时抛出异常取决于实现。实现可以简单地说它不会在此处抛出异常并结束。 (例如考虑具有垃圾回收的实现) - R. Martinho Fernandes
4
@ShafikYaghmour 谁决定你的内存用完了?那是由实现程序来决定的。没有什么可以引用的。你需要找到一句话说实现程序不能够聪明到可以从空气中伪造出内存。或者,就像在这种情况下很好的做法一样,进行一些基本的垃圾回收操作。 - R. Martinho Fernandes
2
我并不认为这是符合 as-if 规则的。一个替代全局 operator new 可以在另一个翻译单元中定义,而调用该函数可能会产生可观察的副作用。 - T.C.
2
在这种特定情况下,没有其他的翻译单元。 - Shafik Yaghmour
4
链接器可能知道没有其他的翻译单元,但编译器并不知道。确实,在这种情况下,副作用会丢失。 - Casey
显示剩余11条评论

9
这里的理由是,机器可以有多少内存没有规定,语言也没有提供检查已分配或空闲内存量的方法(尽管请注意,POSIX确实定义了mallinfo)。在这里,我们在一台具有无限内存的抽象机器上模拟您的程序,其中分配持续成功。或者至少,在此循环中用于分配的内存是无限的,但对于整个程序来说并不是始终如一的。无论如何,我知道两个好的反对意见。
首先,考虑如果是malloc而不是operator new。 C99规范规定:
“malloc函数为大小由size指定且值不确定的对象分配空间。 malloc函数返回null指针或指向分配空间的指针。”
将malloc()编译为始终成功似乎符合该规范。但是,如果您调用它的次数超过我们实际上可以为其创建指针的次数,并且仅在它失败时退出循环,该怎么办?一个可能的解决办法是注意到在抽象机器定义中没有规则规定64位指针只能包含2 64 个可能的值,只是没有提供构造超出此范围的值的方法。看起来实现可以随意创建这些东西。就个人而言,我发现这个答案令人不满意。
考虑到我们还优化了像“T *t1 = new T; T *t2 =(T *)rand();”这样的东西,假设t1可能不是t2的别名。无论rand选择了正确的地址还是您跨越整个地址空间迭代,一旦我们显示t1的地址没有馈送t2,我们就应该能够得出它们指向不同对象的结论。虽然我希望标准就是这样工作的,并且编译器也是这样工作的,但我不知道是否有任何标准文本支持这个观点。这很可能成为未来论文的主题。
其次,operator new不是malloc,而是可替换的函数。正如Casey的回复所建议的那样,我们打算遵循N3664中的规则(尽管我认为clang尚不能将新表达式与operator new的显式调用区别对待)。Shafik指出,这似乎违反了因果关系,但N3664最初起源于N3433,而且我相当确定我们先写了优化,然后才写了论文。

+1 感谢您抽出时间提供这个答案,当编译器实现者或标准委员会的成员回答或评论此类问题时,我总是非常感激。这些事情很少有以充分理解原理的方式记录下来。 - Shafik Yaghmour

5

看起来Clang正在根据N3664澄清内存分配中的新规则进行内存优化,该规则已被纳入C++14标准。 N3664允许通过合并分配或完全消除分配来减少调用分配/取消分配函数的次数。


1
clang 3.2也表现出这种方式,因此除非它被反向移植,否则似乎不太可能。 - Shafik Yaghmour

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接