ChatGPT解决这个技术问题 Extra ChatGPT

编译器可以优化从堆到栈的分配吗?

就编译器优化而言,将堆分配更改为堆栈分配是否合法和/或可能?还是会破坏 as-if rule

例如,假设这是代码的原始版本

{
    Foo* f = new Foo();
    f->do_something();
    delete f;
}

编译器是否能够将其更改为以下内容

{
    Foo f{};
    f.do_something();
}

我不这么认为,因为如果原始版本依赖于自定义分配器之类的东西,那将会产生影响。标准对此有具体说明吗?

不,这太过分了。不断增长的堆栈使用是一件大事,他们确实以它命名了一个流行的编程网站。
Related
Clang 确实优化了这一点,如果它可以内联被调用的函数(可能在函数体上存在一些条件)。 godbolt.org/g/hnAMTZ
从 tobi303 提到的链接来看,自 c++14 以来情况发生了变化,请参见 [expr.new];从 c++14 开始,编译器可以将 Foo 存储在堆栈中,只要它可以证明相同的行为(例如,do_something 中没有抛出任何内容)

g
geza

是的,这是合法的。 C++14 的 expr.new/10

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

expr.delete/7

如果 delete-expression 的操作数的值不是空指针值,则: — 如果对要删除的对象的 new-expression 的分配调用没有省略并且分配没有扩展 (5.3.4) ,删除表达式应调用一个释放函数(3.7.4.2)。从 new-expression 的分配调用返回的值应作为第一个参数传递给释放函数。 — 否则,如果分配是扩展的,或者是通过扩展另一个 new 表达式的分配来提供的,并且由具有扩展的 new 表达式提供的存储的 new 表达式产生的每个其他指针值的删除表达式已被评估,删除表达式应调用释放函数。扩展新表达式的分配调用返回的值应作为第一个参数传递给释放函数。 — 否则,删除表达式将不会调用释放函数 (3.7.4.2)。

因此,总而言之,将 newdelete 替换为定义的实现是合法的,例如使用堆栈而不是堆。

注意:正如 Massimiliano Janes 评论的那样,如果 do_something 抛出,编译器无法完全坚持您的示例的这种转换:在这种情况下,编译器应该省略 f 的析构函数调用(而您的转换后的示例确实在此调用了析构函数案子)。但除此之外,将 f 放入堆栈是自由的。


问题是即使使用了 new,分配是否也可以在堆栈上。如果我理解正确,它将永远是动态内存,永远不会在堆栈上。给定的段落说分配的大小可以扩展到更大的动态内存块,或者使用以前的动态内存扩展。
@SHR:我强调“存储是由实现提供的”。它可以是任何东西,甚至是堆栈。
我对 Is the compiler allowed to optimize out heap memory allocations? 的回答中讨论了这些变化的背景
l
lorro

这些是不等价的。 f.do_something() 可能会抛出,在这种情况下,第一个对象保留在内存中,第二个对象被破坏。


值得注意的是,声明函数 noexcept 对 gcc 和 clang 的优化器没有帮助,但显示函数体有帮助。这可能还有更多。
@BaummitAugen如果您问“为什么编译器不执行此优化”,我认为确实还有更多:当有人编写新表达式时,他们想要动态分配。如果他们想要堆栈分配,他们会写 Foo f{}。这是有正当理由的,编译器无法知道,例如,它们可能在 valgrind 下运行并想要跟踪所有堆使用情况,或者它们正在调试堆碎片问题。编译器必须在允许的优化和编码人员真正想要的之间取得平衡。编译器应该是朋友,而不是敌人
D
Daniel Jour

我想指出 IMO 在其他答案中强调的不够:

struct Foo {
    static void * operator new(std::size_t count) {
        std::cout << "Hey ho!" << std::endl;
        return ::operator new(count);
    }
};

分配 new Foo() 通常不能被替换,因为:

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

因此,与上面的 Foo 示例一样,需要调用 Foo::operator new。省略这个调用会改变程序的可观察行为。

实际示例:Foo 可能需要驻留在某些特殊的内存区域(如内存映射 IO)中才能正常运行。