哪个更快:堆栈分配或堆分配


503

这个问题听起来很基础,但这是我与另一位与我合作的开发人员进行的辩论。

我很小心地在可能的地方堆放东西,而不是堆放东西。他在跟我说话,看着我的肩膀,并评论说没有必要,因为他们是同样的表现。

我一直给人的印象是,增加堆栈的时间是恒定的,并且堆分配的性能取决于堆当前的分配(查找适当大小的孔)和取消分配(折叠孔以减少碎片)的复杂度,因为如果我没记错的话,许多标准库的实现在删除过程中都需要花费一些时间。

这让我感到震惊,因为它可能非常依赖于编译器。特别是对于这个项目,我正在使用Metrowerks编译器用于PPC体系结构。深入了解这种组合将是最有帮助的,但是总的来说,对于GCC和MSVC ++,情况是什么?堆分配的性能不如堆栈分配高吗?没有区别吗?还是差异如此之细,以至于没有意义的微优化。


11
我知道这很古老,但是很高兴看到一些C / C ++片段演示了不同类型的分配。
约瑟夫·魏斯曼

42
您的母牛皮匠非常无知,但更重要的是他很危险,因为他对自己非常无知的事情提出了权威性的主张。尽快从您的团队中请这些人。
吉姆·

5
需要注意的是堆通常是多少比堆越大。如果分配了大量数据,则实际上必须将其放在堆上,否则必须从OS更改堆栈大小。
Paul Draper

1
除非您有基准测试或复杂性参数证明,否则所有优化都是默认的毫无意义的微优化。
比昂·林德奎斯特(BjörnLindqvist)

2
我想知道您的同事是否具有Java或C#的丰富经验。在这些语言中,几乎所有内容都是在后台进行堆分配的,这可能会导致这样的假设。
Cort Ammon

Answers:


493

堆栈分配要快得多,因为它真正要做的只是移动堆栈指针。使用内存池,可以从堆分配中获得可比的性能,但这会带来一点点复杂性和麻烦。

而且,堆栈与堆不仅是性能方面的考虑;它还告诉您很多有关对象的预期寿命的信息。


211
而更重要的是,堆栈始终是热的,你得到的内存更大的可能是在高速缓存比任何远堆中分配的内存
伯努瓦

47
在某些(据我所知,大多数是嵌入式的)体系结构中,堆栈可以存储在快速片上存储器(例如SRAM)中。这可以带来巨大的改变!
莱安德

38
因为堆栈实际上是一个堆栈。您不能释放堆栈使用的大块内存,除非它位于其顶部。没有管理,您可以在上面推送或弹出内容。另一方面,堆内存是受管理的:它向内核请求内存块,也许将它们拆分,合并,重用并释放它们。堆栈实际上是用于快速分配和短期分配的。
贝诺瓦'02

24
@Pacerier因为堆栈比堆小得多。如果要分配大数组,则最好在堆上分配它们。如果您尝试在堆栈上分配一个大数组,则会使您产生堆栈溢出。例如,在C ++中尝试这样做:int t [100000000]; 尝试例如t [10000000] = 10; 然后cout << t [10000000]; 它应该给您一个堆栈溢出,否则将不起作用,也不会显示任何内容。但是,如果您在堆上分配数组:int * t = new int [100000000]; 然后再执行相同的操作,因为堆具有如此大的数组所需的大小,因此它将起作用。
莉莲·莫拉鲁

7
@Pacerier最明显的原因是,在栈上的对象在离开它们在分配的块去的范围了。
吉姆·巴尔特

166

堆栈要快得多。实际上,在大多数架构上,例如在x86上,它仅使用一条指令:

sub esp, 0x10

(这会将堆栈指针向下移动0x10字节,从而“分配”这些字节供变量使用。)

当然,堆栈的大小非常非常有限,因为您会很快发现是否过度使用了堆栈分配或尝试进行递归:-)

另外,没有必要优化不需要验证的代码的性能,例如通过性能分析证明。“过早的优化”通常会引起更多的问题,而不是值得的。

我的经验法则:如果我知道在编译时需要一些数据,并且它的大小不足几百个字节,那么我将对其进行堆栈分配。否则,我会对其进行堆分配。


20
一条指令,通常由堆栈上的所有对象共享。
MSalters

9
提出了要点,尤其是关于可验证需要的要点。我一直对人们对性能的关注错位感到惊讶。
Mike Dunlavey

6
“解除分配”也非常简单,只需一条leave指令即可完成。
doc

15
请记住此处的“隐藏”成本,尤其是第一次扩展堆栈时。这样做可能会导致页面错误,上下文切换到内核,这需要做一些工作来分配内存(或在最坏的情况下从swap加载)。

2
在某些情况下,您甚至可以分配0条指令。如果知道有关需要分配多少字节的信息,则编译器可以在分配其他堆栈变量的同时提前分配它们。在这种情况下,您根本不用支付任何费用!
Cort Ammon

120

老实说,编写一个程序来比较性能很简单:

#include <ctime>
#include <iostream>

namespace {
    class empty { }; // even empty classes take up 1 byte of space, minimum
}

int main()
{
    std::clock_t start = std::clock();
    for (int i = 0; i < 100000; ++i)
        empty e;
    std::clock_t duration = std::clock() - start;
    std::cout << "stack allocation took " << duration << " clock ticks\n";
    start = std::clock();
    for (int i = 0; i < 100000; ++i) {
        empty* e = new empty;
        delete e;
    };
    duration = std::clock() - start;
    std::cout << "heap allocation took " << duration << " clock ticks\n";
}

有人说愚蠢的一致性是小头脑的妖精。显然,优化编译器是许多程序员的专精。该讨论曾经是答案的底部,但是显然人们不愿意阅读那么远的内容,因此,我将其移至此处以避免收到已经回答的问题。

优化的编译器可能会注意到此代码不执行任何操作,并且可能会对其进行优化。做这样的事情是优化器的工作,而与优化器抗争是傻瓜的事。

我建议在关闭优化的情况下编译此代码,因为没有很好的方法来欺骗当前正在使用或将来将要使用的每个优化器。

任何打开优化器然后抱怨与优化器抗争的人都应该受到公众的嘲笑。

如果我关心纳秒精度,我不会使用std::clock()。如果我想将结果发表为博士学位论文,那么我会做更多的事情,并且我可能会比较GCC,Tendra / Ten15,LLVM,Watcom,Borland,Visual C ++,Digital Mars,ICC和其他编译器。实际上,堆分配比栈分配要花费数百倍的时间,而且我认为进一步研究这个问题没有任何用处。

优化器的任务是摆脱我正在测试的代码。我看不出有任何理由告诉优化器运行,然后尝试使优化器欺骗而不进行实际优化。但是,如果我认为这样做有价值,那么我将执行以下一项或多项操作:

  1. 向中添加数据成员empty,并在循环中访问该数据成员;但是,如果我只从数据成员中读取数据,则优化器可以进行不断折叠并删除循环;如果我只写数据成员,则优化器可能会跳过循环的最后一个迭代,而不是最后一个迭代。另外,问题不是“堆栈分配和数据访问与堆分配和数据访问”。

  2. 声明e volatilevolatile通常编译不正确(PDF)。

  3. 获取e循环内部的地址(并可能将其分配给extern在另一个文件中声明和定义的变量)。但是即使在这种情况下,编译器可能会注意到-至少在堆栈上- e总是会分配在相同的内存地址,然后像上面的(1)一样进行常量折叠。我得到了循环的所有迭代,但是从未真正分配对象。

除了显而易见的以外,该测试还存在缺陷,因为它可以同时测量分配和释放,并且最初的问题并未询问释放。当然,分配在堆栈上的变量会在其作用域的末尾自动释放,因此不要调用delete(1)倾斜数字(关于堆栈分配的数字中包括堆栈释放,因此仅测量堆释放是合理的)和( 2)导致非常严重的内存泄漏,除非我们保留对新指针的引用并delete在进行时间测量后调用。

在我的机器上,在Windows上使用g ++ 3.4.4,对于小于100000分配的任何内容,对于堆栈和堆分配,我都会获得“ 0时钟滴答”,即使这样,对于堆栈分配和“ 15时钟滴答”也将获得“ 0时钟滴答”。 ”进行堆分配。当我测量10,000,000个分配时,堆栈分配需要31个时钟滴答,堆分配需要1562个时钟滴答。


是的,优化编译器可能会忽略创建空对象。如果我理解正确,它甚至可能会漏掉整个第一个循环。当我将迭代次数提高到10,000,000时,堆栈分配花费了31个时钟周期,堆分配花费了1562个时钟周期。我认为可以肯定地说,在没有告诉g ++优化可执行文件的情况下,g ++并没有忽略构造函数。


自从我写这篇文章以来,多年来,Stack Overflow一直偏向于发布经过优化的版本的性能。总的来说,我认为这是正确的。但是,我仍然认为在您实际上不希望优化代码时要求编译器优化代码是很愚蠢的。我觉得这与为代客停车支付额外费用非常相似,但拒绝交出钥匙。在这种情况下,我不希望优化程序运行。

使用基准的略微修改版本(以解决原始程序每次都没有通过循环在栈上分配某些东西的有效点),并在不进行优化的情况下进行编译,但链接到发行版库(以解决我们不希望使用的有效点)不想包含任何由于链接到调试库而导致的速度下降):

#include <cstdio>
#include <chrono>

namespace {
    void on_stack()
    {
        int i;
    }

    void on_heap()
    {
        int* i = new int;
        delete i;
    }
}

int main()
{
    auto begin = std::chrono::system_clock::now();
    for (int i = 0; i < 1000000000; ++i)
        on_stack();
    auto end = std::chrono::system_clock::now();

    std::printf("on_stack took %f seconds\n", std::chrono::duration<double>(end - begin).count());

    begin = std::chrono::system_clock::now();
    for (int i = 0; i < 1000000000; ++i)
        on_heap();
    end = std::chrono::system_clock::now();

    std::printf("on_heap took %f seconds\n", std::chrono::duration<double>(end - begin).count());
    return 0;
}

显示:

on_stack took 2.070003 seconds
on_heap took 57.980081 seconds

在我的系统上使用命令行编译时cl foo.cc /Od /MT /EHsc

您可能不同意我获取未优化构建的方法。很好:随时随地修改基准。启用优化后,我得到:

on_stack took 0.000000 seconds
on_heap took 51.608723 seconds

不是因为堆栈分配实际上是瞬时的,而是因为任何半个体面的编译器都可以注意到它on_stack并没有做任何有用的事情,并且可以进行优化。我的Linux笔记本电脑上的GCC还注意到它on_heap并没有做任何有用的事情,并对其进行了优化:

on_stack took 0.000003 seconds
on_heap took 0.000002 seconds

2
另外,您应该在主函数的开始处添加一个“校准”循环,以使您了解每个循环获得多少时间,并调整其他循环,以确保示例可以运行一定的时间,而不是您正在使用的固定常数。
乔·皮内达

2
我也很高兴增加每个选项循环的运行次数(加上指示g ++不进行优化?)产生了明显的结果。因此,现在我们很难说堆栈速度更快。感谢您的努力!
乔·派

7
摆脱这样的代码是优化器的工作。是否有充分的理由打开优化器,然后阻止其实际优化?我已经对答案进行了编辑,以使事情变得更加清晰:如果您喜欢与优化程序抗争,请准备学习聪明的编译器编写者的知识。
Max Lybbert 09年

3
我来晚了,但是在这里也值得一提的是堆分配通过内核请求内存,因此性能的下降也很大程度上取决于内核的效率。使用此代码在Linux(Linux的3.10.7-巴布亚#2 SMP星期三09月04日18时58分21秒MDT 2013 x86_64的),修改为HR计时器,并且在每个循环使用100百万次迭代产生这种性能:stack allocation took 0.15354 seconds, heap allocation took 0.834044 seconds-O0集,制造在我的特定机器上,Linux堆分配仅变慢了约5.5倍。
Taywee

4
在没有优化的Windows(调试版本)上,它将使用调试堆,该调试堆比非调试堆慢得多。我认为“欺骗”优化器并不是一个坏主意。编译器编写者很聪明,但是编译器不是AI的。
保罗

30

我了解到的关于Xbox 360 Xenon处理​​器上的堆栈与堆分配的有趣现象(这可能也适用于其他多核系统)是,在堆上进行分配会导致输入关键部分以暂停所有其他内核,因此分配不会不冲突。因此,在一个紧密的循环中,堆栈分配是固定大小阵列的一种选择,因为它可以防止停顿。

如果您正在为多核/多进程进行编码,则这可能是要考虑的另一种提速方法,因为堆栈分配只能由运行作用域函数的内核查看,而不会影响任何其他内核/ CPU。


4
大多数多核计算机都是如此,而不仅仅是Xenon。甚至Cell也必须这样做,因为您可能正在该PPU内核上运行两个硬件线程。
Crashworks

15
这是堆分配器的实现(特别是较差)的结果。更好的堆分配器不必在每个分配上都获得一个锁。
克里斯·多德

19

您可以为性能非常好的特定大小的对象编写特殊的堆分配器。但是,一般堆分配器的性能不是特别好。

我也同意TorbjörnGyllebring关于物体的预期寿命。好点子!


1
有时也称为平板分配。
Benoit

8

我不认为堆栈分配和堆分配通常是可互换的。我也希望它们两者的性能足以通用。

我强烈建议您购买小件物品,无论哪种物品都更适合分配范围。对于大型项目,可能需要堆。

在具有多个线程的32位操作系统上,堆栈通常受到相当有限的限制(尽管通常至少为几mb),因为需要划分地址空间,一个或多个线程迟早会运行到另一个线程中。在单线程系统(无论如何还是Linux glibc单线程)上,该限制要少得多,因为堆栈可以不断增长。

在64位操作系统上,有足够的地址空间来使线程堆栈很大。


6

通常,堆栈分配仅包括从堆栈指针寄存器中减去。这比搜索堆快了很多吨。

有时堆栈分配需要添加虚拟内存页面。添加零内存的新页面不需要从磁盘读取页面,因此通常这仍然比搜索堆快很多(特别是如果部分堆也被页面调出)。在极少数情况下,您可以构造一个示例,堆中恰好有足够的空间可用,而该堆已经在RAM中,但是为堆栈分配新页必须等待其他页被写出到磁盘。在那种罕见的情况下,堆会更快。


我认为除非分页,否则不会“搜索”堆。可以肯定的是,固态存储器使用多路复用器,并且可以直接访问该存储器,因此可以访问随机访问存储器。
乔·菲利普斯

4
这是一个例子。调用程序要求分配37个字节。库函数查找至少40个字节的块。空闲列表的第一个块有16个字节。空闲列表中的第二个块有12个字节。第三块具有44个字节。此时,图书馆停止搜索。
Windows程序员

6

除了数量级性能优于堆分配之外,对于长时间运行的服务器应用程序,堆栈分配更可取。甚至最好的托管堆最终也会变得分散,以至于应用程序性能下降。


4

堆栈的容量有限,而堆栈没有。进程或线程的典型堆栈约为8K。分配大小后便无法更改。

堆栈变量遵循作用域规则,而堆变量则不遵循。如果指令指针超出函数范围,则与该函数关联的所有新变量都将消失。

最重要的是,您无法预先预测整个函数调用链。因此,仅分配200个字节可能会引起堆栈溢出。如果您要编写的是库而不是应用程序,那么这尤其重要。


1
默认情况下,为现代OS上的用户模式堆栈分配的虚拟地址空间量可能至少为64kB或更大(在Windows上为1MB)。您是在谈论内核堆栈大小吗?
bk1e

1
在我的机器上,进程的默认堆栈大小为8MB,而不是kB。您的计算机多大了?
格雷格·罗杰斯

3

我认为生命周期至关重要,是否必须以复杂的方式构造要分配的事物。例如,在事务驱动的建模中,通常必须填写事务结构并将其传递给具有一系列字段的操作函数。以OSCI SystemC TLM-​​2.0标准为例。

由于构造昂贵,将这些分配在靠近操作的堆栈上往往会导致巨大的开销。好的方法是通过池或简单的策略(例如“此模块永远只需要一个事务对象”)在堆上分配并重用事务对象。

这比在每个操作调用上分配对象快许多倍。

原因很简单,因为该对象具有昂贵的构造和相当长的使用寿命。

我会说:尝试两者,看看哪种方法最适合您,因为它实际上取决于代码的行为。


3

与堆分配相比,堆分配的最大问题可能是,在一般情况下堆分配是无限制的操作,因此,在计时很重要的情况下,您不能使用它。

对于其他时间无关紧要的应用程序,这可能没什么大不了的,但是如果您堆很多,这会影响执行速度。始终尝试将堆栈用于短暂且经常分配的内存(例如在循环中),并尽可能长地使用-在应用程序启动期间进行堆分配。


3

不是jsut堆栈分配更快。使用堆栈变量也能赢得很多好处。它们具有更好的参考位置。最后,重新分配也便宜得多。


3

堆栈分配是一对指令,而我所知最快的rtos堆分配器(TLSF)平均使用150条指令。同样,堆栈分配不需要锁,因为它们使用线程本地存储,这是另一个巨大的性能优势。因此,堆栈分配的速度可以加快2-3个数量级,具体取决于环境中多线程的使用量。

通常,如果您在乎性能,那么堆分配是您的最后选择。可行的中间选项可以是固定池分配器,该分配器也只有几个指令,并且每个分配的开销很小,因此对于较小的固定大小对象非常有用。不利的一面是,它仅适用于固定大小的对象,本质上不是线程安全的,并且存在块碎片问题。


3

C ++语言特有的问题

首先,没有C ++强制要求的所谓“堆栈”或“堆”分配。如果您在谈论块范围内的自动对象,它们甚至不会被“分配”。(顺便说一句,在C中,自动存储的持续时间绝对不同于“分配的”;在C ++中,后者是“动态的”。)动态分配的内存位于空闲存储中,而不一定位于“堆”中,尽管后者通常是(默认)实现

尽管按照抽象的机器语义规则,自动对象仍会占用内存,但是当符合标准的C ++实现可以证明这无关紧要时(当它不改变程序的可观察行为时),可以忽略此事实。该许可由ISO C ++中的as-if规则授予,这也是启用常规优化的通用子句(ISO C中也存在几乎相同的规则)。除了as-if规则外,ISO C ++还具有复制省略规则允许省略特定对象的创建。从而省略了所涉及的构造函数和析构函数调用。结果,与源代码所隐含的天真的抽象语义相比,这些构造函数和析构函数中的自动对象(如果有的话)也被消除了。

另一方面,按设计,免费商店的分配绝对是“分配”。根据ISO C ++规则,可以通过调用分配函数分配。但是,自ISO C ++ 14起,有一个新的(不作为原状)规则允许::operator new在特定情况下合并全局分配函数(即)调用。因此,动态分配操作的某些部分也可以像自动对象一样无操作。

分配功能分配内存资源。可以基于分配器使用分配器进一步分配对象。对于自动对象,它们是直接呈现的-尽管可以访问基础内存并用于为其他对象提供内存(通过放置new),但这对于自由存储来说意义不大,因为无法移动内存。其他资源。

所有其他问题不在C ++的范围之内。但是,它们仍然很重要。

关于C ++的实现

C ++不会公开标准化的激活记录或某种一流的延续(例如,著名的 call/cc),无法直接操作激活记录帧-实现需要将自动对象放置到其中。一旦与基础实现(“本机”非便携式代码,例如内联汇编代码)不存在(非便携式)互操作,则忽略框架的基础分配可能就变得微不足道了。例如,当内联被调用函数时,这些框架可以有效地合并到其他框架中,因此无法显示什么是“分配”。

但是,一旦尊重互操作性,事情就会变得复杂。C ++的典型实现将通过一些调用约定(作为与本机(ISA级计算机)代码共享的二进制边界)公开ISA(指令集体系结构)上的互操作能力。特别是在维护堆栈指针(这通常由ISA级寄存器直接保存(可能需要访问特定的机器指令))时,这显然是昂贵的。堆栈指针指示(当前活动的)函数调用的顶部帧的边界。当输入函数调用时,需要一个新的帧,并且堆栈指针要增加或减去(取决于ISA的约定)一个不小于所需帧大小的值。然后说框架分配当堆栈指针执行完之后。根据用于调用的调用约定,函数的参数也可以传递到堆栈框架上。框架可以保存由C ++源代码指定的自动对象(可能包括参数)的内存。从这种实现的意义上说,这些对象是“分配的”。当控件退出函数调用时,不再需要该帧,通常通过将堆栈指针恢复到调用之前的状态(根据调用约定先前保存的状态)来释放该帧。这可以视为“重新分配”。这些操作使激活记录有效地成为LIFO数据结构,因此通常称为“ (调用)堆栈 ”。

由于大多数C ++实现(尤其是针对ISA级本机代码并使用汇编语言作为其直接输出的实现)都使用类似的策略,因此,这种令人困惑的“分配”方案很受欢迎。这样的分配(以及解除分配)的确花费了机器周期,并且(非优化的)调用频繁发生时,即使现代的CPU微体系结构可以通过硬件为通用代码模式实现复杂的优化(例如使用堆栈引擎的实现 PUSH/ POP说明)。

但是总而言之,总的来说,堆栈帧分配的成本要比调用免费存储空间的分配函数的调用要少得多(除非对其进行了完全优化),该函数本身可以具有数百个(如果不是上百万个) :-)维护堆栈指针和其他状态的操作。分配功能通常基于托管环境提供的API(例如,操作系统提供的运行时)。与为函数调用保留自动对象的目的不同,此类分配是通用的,因此它们不会像堆栈一样具有框架结构。传统上,它们从称为的池存储中分配空间,(或几个堆)。与“堆栈”不同,此处的“堆”概念并不表示正在使用的数据结构。它源自数十年前的早期语言实现。(顺便说一句,在程序或线程启动时,调用栈通常由环境从堆中分配固定大小或用户指定的大小。)用例的性质使得从堆中进行分配和释放的过程要复杂得多(比push或pop of堆栈帧),并且几乎不可能通过硬件直接进行优化。

对内存访问的影响

通常的堆栈分配总是将新框架放在顶部,因此它具有很好的局部性。这对缓存很友好。OTOH,在免费存储区中随机分配的内存没有这种属性。自ISO C ++ 17以来,存在由提供的池资源模板<memory>。这种接口的直接目的是允许连续分配的结果在内存中紧密排列在一起。这承认了这样一个事实,即该策略通常对当代实现具有良好的性能,例如,易于在现代体系结构中进行缓存。不过,这是关于访问的性能而不是分配

并发

期望并发访问内存可能在堆栈和堆之间产生不同的影响。调用堆栈通常由C ++实现中的一个执行线程专有。OTOH,堆通常在进程中的线程之间共享。对于此类堆,分配和释放函数必须保护共享的内部管理数据结构免受数据争用。结果,由于内部同步操作,堆分配和释放可能会产生额外的开销。

空间效率

由于用例和内部数据结构的性质,堆可能会遭受内部内存碎片的困扰,而堆栈则不会。这对内存分配的性能没有直接影响,但是在具有虚拟内存的系统中,低空间效率可能会降低内存访问的整体性能。当将HDD用作物理内存交换时,这尤其糟糕。这可能会导致相当长的延迟-有时数十亿个周期。

堆栈分配的局限性

尽管实际上堆栈分配在性能上通常比堆分配要好,但这并不意味着堆栈分配总是可以代替堆分配。

首先,无法使用ISO C ++以可移植的方式在运行时在堆栈上分配具有指定大小的空间。allocaG ++的VLA(可变长度数组)之类的实现提供了一些扩展,但是有避免它们的理由。(IIRC,Linux源代码最近取消了对VLA的使用。)(还请注意,ISO C99确实强制要求使用VLA,但ISO C11则使该支持成为可选。)

其次,没有可靠且可移植的方法来检测堆栈空间耗尽。这通常称为堆栈溢出(此站点的词源),但更准确地说,可能是堆栈溢出。实际上,这通常会导致无效的内存访问,然后程序的状态将被破坏(...或更糟的是有安全漏洞)。实际上,ISO C ++没有“堆栈”的概念,并且在资源耗尽时使其变为未定义的行为。注意自动对象应留多少空间。

如果堆栈空间用完,则堆栈中分配的对象过多,这可能是由于调用函数过多或自动对象使用不当所致。这种情况可能表明存在错误,例如没有正确退出条件的递归函数调用。

尽管如此,有时还是需要深度递归调用。在需要支持未绑定活动调用的语言的实现中(其中调用深度仅受总内存限制),不可能像典型的C ++实现一样直接使用(当代)本机调用堆栈作为目标语言激活记录。要变通解决此问题,需要替代方法来创建激活记录。例如,SML / NJ在堆上显式分配帧并使用仙人掌堆栈。这种激活记录帧的复杂分配通常不如调用堆栈帧那么快。但是,如果在保证适当的尾部递归的,则对象语言中的直接堆栈分配(即该语言中的“对象”没有存储为引用,但是可以一对一映射到未共享的C ++对象的本机原始值)变得越来越复杂。总体上的性能损失。使用C ++实现此类语言时,很难估计性能影响。


像stl一样,越来越少愿意传播这些概念。cppcon2018上的许多功能部件也heap经常使用。
陈力

@陈力在记住某些特定实现的情况下,“堆”可以是明确的,因此有时可以。不过,“一般而言”是多余的。
FrankHB

什么是互操作?
陈力

@陈力我的意思是C ++源代码中涉及的任何“本机”代码互操作,例如,任何内联汇编代码。这依赖于C ++未涵盖的(ABI的)假设。COM互操作(基于某些Windows特定的ABI)或多或少相似,尽管它对C ++大多是中立的。
FrankHB '18

2

关于这种优化有一个总体要点。

您获得的优化与程序计数器实际在该代码中的时间成正比。

如果对程序计数器进行采样,则会发现它在哪里花费时间,而这通常只是代码的一小部分,而且通常在库例程中您无法控制。

只有发现它在对象的堆分配中花费大量时间,才可以明显更快地对其进行堆栈分配。


2

堆栈分配几乎总是与堆分配一样快或更快,尽管堆分配器当然可以简单地使用基于堆栈的分配技术。

但是,在处理基于堆栈的分配与基于堆的分配的整体性能时(或更好地说,本地分配与外部分配),存在更大的问题。通常,堆(外部)分配很慢,因为它正在处理许多不同种类的分配和分配模式。减小正在使用的分配器的范围(使其在算法/代码局部)将有助于提高性能,而无需进行任何重大更改。为分配模式添加更好的结构,例如,通过以更简单,更结构化的方式使用分配器,对分配和解除分配对强制执行LIFO排序也可以提高分配器的性能。或者,您可以使用或编写针对您的特定分配模式调整的分配器;大多数程序会频繁分配一些离散的大小,因此,基于几个固定(最好是已知)大小的后备缓冲区的堆将表现得非常好。Windows正是出于这个原因使用了低碎片整理堆。

另一方面,如果线程太多,则在32位内存范围上基于堆栈的分配也会带来危险。堆栈需要一个连续的内存范围,因此拥有的线程越多,运行它们就需要更多的虚拟地址空间,而不会导致堆栈溢出。对于64位而言,这暂时不会是一个问题,但是它肯定会在具有许多线程的长时间运行的程序中造成严重破坏。由于碎片而使虚拟地址空间用尽总是很难解决的。


我不同意你的第一句话。
brian beuning 2016年

2

正如其他人所说,堆栈分配通常要快得多。

但是,如果对象的复制成本很高,则在不小心的情况下使用对象时,在堆栈上进行分配可能会导致性能严重下降。

例如,如果您在堆栈上分配一些东西,然后将其放入容器中,则最好在堆上分配并将指针存储在容器中(例如,使用std :: shared_ptr <>)。如果您要按值传递或返回对象,以及其他类似情况,也是如此。

关键是,尽管在许多情况下堆栈分配通常比堆分配要好,但是有时如果在最不适合计算模型的情况下放弃堆栈分配的方法,则可能导致更多的问题无法解决。


2
class Foo {
public:
    Foo(int a) {

    }
}
int func() {
    int a1, a2;
    std::cin >> a1;
    std::cin >> a2;

    Foo f1(a1);
    __asm push a1;
    __asm lea ecx, [this];
    __asm call Foo::Foo(int);

    Foo* f2 = new Foo(a2);
    __asm push sizeof(Foo);
    __asm call operator new;//there's a lot instruction here(depends on system)
    __asm push a2;
    __asm call Foo::Foo(int);

    delete f2;
}

在asm中就是这样。当您进入时funcf1and指针f2已分配在堆栈上(自动存储)。顺便说一句,美孚f1(a1)对堆栈指针(无指示作用esp),它已被分配,如果func欲望得到成员f1,它的指令是这样的:lea ecx [ebp+f1], call Foo::SomeFunc()。堆栈分配的另一件事可能使某人认为内存是类似的FIFO,这FIFO只是在您进入某个函数时发生的,如果您在函数中分配类似的东西int i = 0,则不会发生推送。


1

之前已经提到过,堆栈分配只是在移动堆栈指针,即在大多数体系结构上只有一条指令。将其与堆分配情况下通常发生的情况进行比较。

操作系统将空闲内存的部分保持为链接列表,并带有有效载荷数据,该有效载荷数据由指向空闲部分的起始地址的指针和空闲部分的大小组成。要分配X个字节的内存,将遍历链接列表,并按顺序访问每个注释,以查看其大小是否至少为X。当找到大小P> = X的部分时,P分为两部分,尺寸X和PX。链接列表将更新,并返回指向第一部分的指针。

如您所见,堆分配取决于可能的因素,例如请求的内存量,内存的碎片程度等等。


1

通常,栈分配比堆分配更快,正如上面几乎每个答案所述。堆栈推入或弹出操作为O(1),而从堆进行分配或释放则可能需要遍历先前的分配。但是,通常不应该在性能密集的紧凑循环中进行分配,因此选择通常取决于其他因素。

进行这种区分可能会很好:您可以在堆上使用“堆栈分配器”。严格来说,我将堆栈分配表示实际的分配方法,而不是分配的位置。如果您在实际的程序堆栈上分配了很多东西,由于各种原因,这可能是不好的。另一方面,尽可能使用堆栈方法在堆上分配是分配方法的最佳选择。

由于您提到了Metrowerks和PPC,所以我猜您是说Wii。在这种情况下,内存非常宝贵,并且在可能的情况下使用堆栈分配方法可以确保您不会在片段上浪费内存。当然,与“常规”堆分配方法相比,这样做需要更多的注意。评估每种情况的权衡是明智的。


1

请注意,选择堆栈与堆分配时,通常不考虑速度和性能。堆栈就像堆栈一样,这意味着它非常适合推入块,然后以后进先出的方式再次弹出。程序的执行也是堆栈式的,最后输入的程序首先退出。在大多数编程语言中,过程中所需的所有变量仅在过程执行期间可见,因此它们在进入过程时被压入,并在退出或返回时从堆栈中弹出。

现在来看一个不能使用堆栈的示例:

Proc P
{
  pointer x;
  Proc S
  {
    pointer y;
    y = allocate_some_data();
    x = y;
  }
}

如果您在过程S中分配了一些内存并将其放在堆栈上,然后退出S,则分配的数据将从堆栈中弹出。但是P中的变量x也指向该数据,因此x现在指向具有未知内容的堆栈指针下方的某个位置(假定堆栈向下增长)。如果只是向上移动堆栈指针而不清除其下面的数据,则内容可能仍然存在,但是如果您开始在堆栈上分配新数据,则指针x实际上可能指向该新数据。


0

切勿过早假设,因为其他应用程序代码和用法会影响您的功能。因此,将功能视为隔离是没有用的。

如果您对应用程序很认真,则可以使用VTune或使用任何类似的性能分析工具查看热点。

Ketan


-1

我想说的是,实际上GCC生成的代码(我也记得VS)没有开销来进行堆栈分配

说以下功能:

  int f(int i)
  {
      if (i > 0)
      {   
          int array[1000];
      }   
  }

以下是生成的代码:

  __Z1fi:
  Leh_func_begin1:
      pushq   %rbp
  Ltmp0:
      movq    %rsp, %rbp
  Ltmp1:
      subq    $**3880**, %rsp <--- here we have the array allocated, even the if doesn't excited.
  Ltmp2:
      movl    %edi, -4(%rbp)
      movl    -8(%rbp), %eax
      addq    $3880, %rsp
      popq    %rbp
      ret 
  Leh_func_end1:

因此,无论您有多少局部变量(即使在if或switch内),也只有3880会更改为另一个值。除非您没有局部变量,否则只需要执行此指令即可。因此分配局部变量没有开销。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.