Which is faster: Stack allocation or Heap allocation
这个问题听起来很简单,但这是我和另一个开发人员的一个争论。
我在尽可能多的地方堆分配东西,而不是堆分配它们。他在和我说话,看着我的肩膀,说没必要这样做,因为他们在表演方面是一样的。
我一直认为增加堆栈的时间是恒定的,堆分配的性能取决于当前堆的复杂度,包括分配(查找适当大小的孔)和取消分配(折叠孔以减少碎片,因为许多标准库实现在删除期间都需要时间来完成此操作如果我没弄错的话)。
这对我来说可能是非常依赖于编译器的。特别是对于这个项目,我使用的是用于PPC体系结构的Metrowerks编译器。洞察这种组合将是最有用的,但一般来说,对于GCC和MSVC++,情况如何?堆分配是否不像堆栈分配那样高?没有区别吗?或者差异是如此微小,以至于变成了无意义的微观优化。
堆栈分配更快,因为它真正做的只是移动堆栈指针。使用内存池,您可以从堆分配中获得类似的性能,但这会增加一点复杂性和它自己的麻烦。
此外,stack与heap不仅要考虑性能,还可以告诉您很多关于对象预期寿命的信息。
叠加速度快得多。它实际上只在大多数架构上使用一条指令,在大多数情况下,例如在x86上:
1 | sub esp, 0x10 |
(将堆栈指针向下移动0x10字节,从而"分配"这些字节供变量使用。)
当然,堆栈的大小是非常有限的,因为您很快就会发现是否过度使用了堆栈分配或尝试执行递归:—)
此外,没有什么理由优化不需要它的代码的性能,例如通过分析来演示。"过早的优化"往往会导致比其价值更多的问题。
我的经验法则是:如果我知道我在编译时需要一些数据,并且它的大小在几百字节以下,那么我就通过堆栈来分配它。否则我会堆分配它。
老实说,编写一个程序来比较性能是很简单的:好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | #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 "; 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 "; } |
据说一个愚蠢的一致性是小头脑的妖精。显然,优化编译器是许多程序员头脑中的妖怪。这个讨论曾经是答案的最底层,但是人们显然不愿意读那么远,所以我把它移到这里来避免得到我已经回答过的问题。好的。
优化编译器可能会注意到这段代码什么也不做,并且可能会完全优化它。优化器的工作就是做那样的事情,而与优化器作战是一件蠢事。好的。
我建议在关闭优化的情况下编译此代码,因为没有好的方法可以欺骗当前正在使用或将来将要使用的每个优化器。好的。
任何打开优化器然后抱怨与之抗争的人都应该受到公众的嘲笑。好的。
如果我关心纳秒级的精度,我就不会使用
优化器的任务是去掉我正在测试的代码。我看不出有任何理由告诉优化器运行,然后试图欺骗优化器不真正优化。但如果我看到这样做的价值,我会做以下一项或多项:好的。
向
声明
在循环中获取
除此之外,这个测试还有一个缺陷,即它度量了分配和解除分配,而最初的问题并没有询问解除分配。当然,在堆栈上分配的变量是在其作用域的末尾自动释放的,因此不调用
在我的机器上,使用Windows上的G++3.4.4,对于任何小于100000个分配的堆栈和堆分配,我都会得到"0时钟滴答",对于堆栈分配,我甚至会得到"0时钟滴答",对于堆分配,我会得到"15时钟滴答"。当我度量10000000个分配时,堆栈分配占用31个时钟周期,而堆分配占用1562个时钟周期。好的。
是的,优化编译器可以省略创建空对象。如果我理解正确,它甚至可以省略整个第一个循环。当我将迭代次数增加到10000000次时,堆栈分配占用了31个时钟周期,而堆分配占用了1562个时钟周期。我认为可以肯定地说,在不告诉g++优化可执行文件的情况下,g++没有省略构造函数。好的。
在我写这篇文章的几年里,对堆栈溢出的偏好是从优化的构建中发布性能。一般来说,我认为这是正确的。但是,我仍然认为当您实际上不希望代码被优化时,要求编译器优化代码是愚蠢的。我觉得这和为代客泊车支付额外费用非常相似,但拒绝交钥匙。在这种特殊情况下,我不希望优化器运行。好的。
使用稍微修改过的基准测试版本(以解决原始程序每次都没有通过循环在堆栈上分配某些内容的有效点),在没有优化的情况下进行编译,但链接到发布库(以解决有效点,我们不希望包括链接到调试库导致的任何减速):好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | #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 ", 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 ", std::chrono::duration<double>(end - begin).count()); return 0; } |
显示器:好的。
1 2 | on_stack took 2.070003 seconds on_heap took 57.980081 seconds |
在我的系统上,当用命令行
您可能不同意我获取非优化构建的方法。很好:您可以随意修改基准。当我打开优化时,我得到:好的。
1 2 | on_stack took 0.000000 seconds on_heap took 51.608723 seconds |
这不是因为堆栈分配实际上是即时的,而是因为任何半熟的编译器都可以注意到
1 2 | on_stack took 0.000003 seconds on_heap took 0.000002 seconds |
好啊。
关于Xbox 360 Xenon处理器上的堆栈与堆分配,我学到了一件有趣的事情,它也适用于其他多核系统,那就是在堆上分配会导致进入一个关键部分来停止所有其他核心,这样分配就不会发生冲突。因此,在一个严格的循环中,堆栈分配是固定大小数组的一种方式,因为它可以防止暂停。
如果您正在为多核/多进程编码,这可能是另一个需要考虑的加速,因为您的堆栈分配只能由运行作用域函数的核心查看,并且不会影响任何其他核心/CPU。
您可以为非常有性能的对象的特定大小编写一个特殊的堆分配器。但是,一般的堆分配器的性能并不特别好。
我也同意Torbj?RN Gylel带来了对象的预期寿命。好点!
我不认为堆栈分配和堆分配通常是可交换的。我也希望它们的性能能够满足一般的使用要求。
我强烈推荐小商品,以更适合分配范围的为准。对于大型项目,堆可能是必需的。
在有多个线程的32位操作系统上,堆栈通常是相当有限的(尽管通常至少只有几个MB),因为地址空间需要被分割,一个线程堆栈迟早会运行到另一个线程堆栈中。在单线程系统(linux-glibc-single-threaded)上,限制要小得多,因为堆栈可以增长。
在64位操作系统上,有足够的地址空间使线程堆栈相当大。
通常,堆栈分配只包括从堆栈指针寄存器中减去。这比搜索堆快很多。
有时,堆栈分配需要添加一页虚拟内存。添加一个新的零内存页面不需要从磁盘读取页面,因此通常这比搜索堆要快很多(特别是如果堆的一部分也被调出了)。在极少数情况下,您可以构建这样一个示例,恰好在RAM中已经存在的堆的一部分中有足够的空间可用,但是为堆栈分配新页必须等待其他页写入磁盘。在这种罕见的情况下,堆更快。
除了比堆分配具有数量级的性能优势之外,对于长时间运行的服务器应用程序,堆栈分配更可取。即使是最好的管理堆最终也会变得支离破碎,应用程序性能也会下降。
堆栈的容量有限,而堆的容量不有限。进程或线程的典型堆栈大约为8K。一旦分配了该堆栈,就无法更改其大小。
堆栈变量遵循作用域规则,而堆变量则不遵循。如果指令指针超出了某个函数,则与该函数关联的所有新变量都将消失。
最重要的是,您不能提前预测整个函数调用链。因此,仅在您的部分分配200字节可能会导致堆栈溢出。如果您正在编写库,而不是应用程序,这一点尤其重要。
堆栈分配是一对指令,而我所知道的最快的RTOS堆分配程序(TLSF)平均使用150条指令。另外,堆栈分配不需要锁,因为它们使用线程本地存储,这是另一个巨大的性能优势。因此,根据多线程环境的严重程度,堆栈分配可以快2-3个数量级。
一般来说,如果您关心性能,那么堆分配是最后的手段。一个可行的中间选项可以是一个固定池分配器,它也只是一对指令,并且每个分配的开销很小,因此对于小的固定大小的对象来说非常有用。其缺点是,它只适用于固定大小的对象,不具有线程安全性,并且存在块碎片问题。
堆栈分配几乎总是和堆分配一样快或更快,尽管对于堆分配程序来说,可以简单地使用基于堆栈的分配技术。
但是,在处理堆栈与基于堆的分配的总体性能时,存在更大的问题(或者在稍微好一点的情况下,本地与外部分配)。通常,堆(外部)分配很慢,因为它处理许多不同类型的分配和分配模式。减少正在使用的分配器的范围(使其成为算法/代码的局部)将有助于提高性能而不进行任何重大更改。为分配模式添加更好的结构,例如,强制对分配和释放对进行后进先出排序,也可以通过更简单和更结构化的方式使用分配器来提高分配器的性能。或者,您可以使用或编写针对特定分配模式进行调优的分配器;大多数程序经常分配一些离散大小,因此基于几个固定(最好是已知)大小的lookaside缓冲区的堆将运行得非常好。因为这个原因,Windows使用它的低碎片堆。
另一方面,如果线程太多,基于32位内存范围的堆栈分配也充满了危险。堆栈需要一个连续的内存范围,因此拥有的线程越多,在没有堆栈溢出的情况下运行它们所需的虚拟地址空间就越多。这对于64位来说(目前)不是问题,但它肯定会给长时间运行的具有大量线程的程序造成严重破坏。由于碎片导致虚拟地址空间不足一直是一个难题。
堆分配与堆栈分配的最大问题可能是,在一般情况下,堆分配是一个无边界操作,因此不能在计时问题时使用它。
对于其他不存在计时问题的应用程序来说,这可能没有那么重要,但是如果您进行大量的堆分配,这将影响执行速度。在应用程序启动期间,始终尝试将堆栈用于寿命短且经常分配的内存(例如在循环中),并尽可能长地执行堆分配。
我认为生命是至关重要的,分配的东西是否必须以复杂的方式构建。例如,在事务驱动的建模中,您通常需要填写一个事务结构,并将一组字段传递给操作函数。以OSCI Systemc TLM-2.0标准为例。
在靠近操作调用的堆栈上分配这些资源往往会导致巨大的开销,因为构建成本很高。在堆上分配和重用事务对象的好方法是通过池或类似"此模块永远只需要一个事务对象"的简单策略。
这比在每个操作调用上分配对象快很多倍。
原因很简单,对象的构造昂贵,使用寿命相当长。
我会说:尝试两者,看看在您的情况下什么最有效,因为它实际上取决于您的代码的行为。
不是JSUT堆栈分配更快。在使用堆栈变量方面,您也赢得了很多。它们有更好的参考位置。最后,取消交易也要便宜得多。
正如其他人所说,堆栈分配通常要快得多。
但是,如果对象的复制成本很高,那么在堆栈上进行分配可能会在以后不小心使用对象时导致巨大的性能损失。
例如,如果您在堆栈上分配一些内容,然后将其放入容器中,则最好在堆上分配并将指针存储在容器中(例如,使用std::shared_ptr<>)。如果您正在按值传递或返回对象以及其他类似的场景,则同样的情况也是如此。
重点是,尽管在许多情况下,堆栈分配通常比堆分配更好,但有时如果在堆栈分配不适合计算模型的情况下,您会不遗余力地进行它的分配,那么它会导致比它解决的问题更多的问题。
对于这样的优化,有一个普遍的观点需要指出。
您得到的优化与程序计数器在该代码中的实际时间成正比。
如果您对程序计数器进行采样,您将发现它花费的时间在哪里,而这通常在代码的一小部分中,并且通常在您无法控制的库例程中。
只有当您发现它在对象的堆分配中花费了大量时间时,堆栈分配它们的速度才会明显更快。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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中是这样的。当您在
前面已经提到,堆栈分配只是移动堆栈指针,即大多数架构上的一条指令。将其与堆分配情况下通常发生的情况进行比较。
操作系统将部分可用内存作为链接列表进行维护,有效负载数据由指向可用部分起始地址的指针和可用部分大小组成。为了分配x字节的内存,将遍历链接列表并按顺序访问每个注释,检查其大小是否至少为x。当找到大小为p>=x的部分时,将p拆分为大小为x和p-x的两部分。更新链接列表并返回指向第一部分的指针。
如您所见,堆分配取决于一些可能的因素,比如您请求的内存量、内存的碎片程度等等。
特定于C++语言的关注点
首先,没有所谓的"堆栈"或"堆"分配的C++命令。如果您谈论的是块范围内的自动对象,那么它们甚至不会被"分配"。(BTW),C中的自动存储持续时间与"分配"完全不相同;后者在C++语句中是"动态的"。动态分配的内存位于空闲存储区上,不一定在"堆"上,尽管后者通常是(默认)实现。好的。
尽管根据抽象语义规则,自动对象仍然占用内存,但当它可以证明这并不重要时,允许一致的C++实现忽略这个事实(当它不改变程序的可观察行为)时。这个权限是由ISO C++中的IF规则授予的,它也是通用的子句,能够实现通常的优化(并且在ISOC中也有几乎相同的规则)。除了类似规则,ISO C++还具有复制删除规则,以允许忽略对象的特定创建。因此省略了所涉及的构造函数和析构函数调用。因此,与源代码所隐含的原始抽象语义相比,这些构造函数和析构函数中的自动对象(如果有)也被消除。好的。
另一方面,通过设计,免费商店分配绝对是"分配"。在ISO C++规则下,这样的分配可以通过分配函数的调用来实现。然而,由于ISO C++ 14,有一种新的(非IF)规则允许在特定情况下合并全局分配函数(即EDCOX1×0)。因此,动态分配操作的某些部分也可能与自动对象的情况不同。好的。
分配函数分配内存资源。可以使用分配器根据分配进一步分配对象。对于自动对象,它们是直接呈现的——尽管底层内存可以被访问并用于为其他对象提供内存(通过放置
所有其他的关注都超出了C++的范围。然而,它们仍然具有重要意义。好的。关于C++的实现
C++没有公开化化的激活记录或一些一流的连续性(例如,通过著名的EDCOX1〔2〕),没有办法直接操作激活记录帧——实现需要放置自动对象的框架。一旦没有(不可移植的)与底层实现的互操作("本机"不可移植的代码,如内联程序集代码),那么忽略底层的帧分配就非常简单。例如,当被调用的函数是内联的时,帧可以有效地合并到其他帧中,因此无法显示什么是"分配"。好的。
然而,一旦互操作得到尊重,事情就会变得复杂起来。C++的一个典型实现将暴露在ISA(指令集体系结构)上的互操作能力,其中一些调用约定是与本机(ISA级机器)代码共享的二进制边界。这将显式地代价高昂,尤其是在维护堆栈指针时,堆栈指针通常由一个ISA级寄存器直接持有(可能需要访问特定的机器指令)。堆栈指针指示(当前活动的)函数调用的顶部框架的边界。当一个函数调用被输入时,需要一个新的帧,栈指针被一个不小于所需帧大小的值加上或减去(取决于is a的约定)。然后当栈指针在操作之后分配时,就表示分配了帧。函数的参数也可以传递到堆栈框架上,这取决于用于调用的调用约定。该框架可以保存由C++源代码指定的自动对象(可能包括参数)的内存。在这种实现的意义上,这些对象是"分配的"。当控件退出函数调用时,不再需要该帧,通常通过将堆栈指针还原回调用前的状态(之前根据调用约定保存)来释放该帧。这可以被视为"解除分配"。这些操作使激活记录有效地成为后进先出的数据结构,因此通常称为"调用堆栈"。堆栈指针有效地指示堆栈的顶部位置。好的。
因为大多数C++实现(特别是针对ISA级本地代码和使用汇编语言作为其直接输出的)使用类似的策略,这样混乱的"分配"方案是流行的。这种分配(以及解除分配)确实花费了机器周期,而且当(非优化)调用频繁发生时,成本可能很高,即使现代CPU微体系结构可以通过硬件为通用代码模式实现复杂的优化(例如,在实现
但无论如何,总的来说,堆栈帧分配的成本明显低于对运行自由存储的分配函数的调用(除非它完全优化掉了),它本身可以有数百(如果不是数百万):-)个操作来维护堆栈指针和其他状态。分配函数通常基于宿主环境提供的API(例如,OS提供的运行时)。与为函数调用保存自动对象的目的不同,这种分配是通用的,因此它们不像堆栈那样具有框架结构。传统上,它们从称为堆(或多个堆)的池存储中分配空间。与"堆栈"不同,这里的"堆"概念并不表示正在使用的数据结构;它是从几十年前的早期语言实现中派生出来的。(顺便说一句,调用堆栈通常由程序或线程启动时的环境从堆中分配固定或用户指定的大小。)用例的性质使得从堆中分配和释放的过程要复杂得多(而不是推送或弹出堆栈帧),而且很难直接由硬件优化。好的。对内存访问的影响
通常的堆栈分配总是将新的帧放在顶部,因此它有一个相当好的位置。这对缓存很友好。但是,在自由存储中随机分配的内存没有这样的属性。自从ISO C++ 17以来,EDOCX1和0提供了池资源模板。这种接口的直接目的是允许连续分配的结果在内存中紧密地联系在一起。这承认了这样一个事实,即这种策略对于当代实现的性能通常是很好的,例如对现代体系结构中的缓存很友好。不过,这是关于访问性能而不是分配的问题。好的。并发性
对内存并发访问的期望可能在堆栈和堆之间产生不同的影响。调用堆栈通常由C++执行中的一个执行线程独占。但是,堆通常在进程中的线程之间共享。对于此类堆,分配和释放函数必须保护共享的内部管理数据结构不受数据争用的影响。因此,由于内部同步操作,堆分配和释放可能会有额外的开销。好的。空间效率
由于用例和内部数据结构的性质,堆可能会遭受内部内存碎片,而堆栈则不会。这不会直接影响内存分配的性能,但在具有虚拟内存的系统中,低空间效率可能会降低内存访问的总体性能。当HDD被用作物理内存的交换时,这种情况尤其糟糕。它会导致相当长的延迟-有时是数十亿个周期。好的。 大学的堆栈分配限制
尽管堆栈分配往往是在堆上分配的性能比在现实中,它不一定总的堆栈分配不均replace堆分配。
好。
第一,有没有办法allocate堆栈空间的大小与一个specified运行在一个可移植的方式与ISO C + +。有扩展的实现等
好。
第二,有没有可靠和可移植的方式检测exhaustion堆栈空间。这往往是所谓的堆栈溢出(嗯,etymology本网站),但可能更精确,"overrun栈"。在现实的原因往往是无效的存取的存储器和程序状态的冰corruptied(...or也许是安全的。(孔)。事实上,ISO C + +有没有栈的观念和行为使得它undefined当资源枯竭型冰淇淋。是关于我多么cautious左室应该自动对象。
好。
如果堆栈空间跑出来,有太多的allocated对象的堆栈,这可能是导致城市主动要求太多的大学improper函数或使用自动对象。用例可以知道存在这样的错误,例如一个递归函数调用没有正确的退出条件。
好。
不过,深desired有时是递归的保证金。在实现等大学语言学院unbound主动要求支持呼叫的呼叫深度有限的城市总只读存储器),它是不可能使用本地调用堆栈直接作为记录语言的目标激活的几种典型的C++类的实现等。例如,[没有allocates帧/ NJ的堆和栈的辨别的仙人掌。"活化"这样的复杂的配置记录的冰槽固定帧通常为《Call堆栈帧。然而,当继续实施适当的语言与直尾递归,堆栈分配的对象(这是语言中,"对象"的语言不可存储为原始值的证明人,但是这可能是一个mapped独享一个C + +对象)的冰更复杂和更平衡的性能,在一般的刑罚。当使用C + +两个实施这样的语言,它是难估计性能的影响。
好。 好的。
一般来说,堆栈分配比上面每个答案提到的堆分配快。堆栈推送或POP是O(1),而从堆中分配或释放可能需要执行以前的分配。然而,您通常不应该在紧凑的、性能密集的循环中进行分配,因此选择通常取决于其他因素。
这样做可能很好:您可以在堆上使用"堆栈分配器"。严格地说,我把栈分配理解为实际的分配方法,而不是分配的位置。如果您在实际的程序堆栈上分配了很多东西,那么由于各种原因,这可能是不好的。另一方面,如果可能的话,使用堆栈方法在堆上进行分配是对分配方法的最佳选择。
既然你提到了Metrowerks和PPC,我猜你的意思是wii。在这种情况下,内存是非常昂贵的,只要可能,使用堆栈分配方法就可以保证不会在片段上浪费内存。当然,这样做比"普通"堆分配方法需要更多的关注。评估每种情况的权衡是明智的。
请注意,在选择堆栈与堆分配时,考虑的通常不是速度和性能。堆栈的作用类似于堆栈,这意味着它非常适合推送块并再次弹出它们,最后一个进入,首先出来。过程的执行也是堆栈式的,输入的最后一个过程是首先退出的。在大多数编程语言中,过程中所需的所有变量只在过程执行期间可见,因此在进入过程时被推送,在退出或返回时从堆栈中弹出。
现在举一个不能使用堆栈的例子:
1 2 3 4 5 6 7 8 9 10 | Proc P { pointer x; Proc S { pointer y; y = allocate_some_data(); x = y; } } |
如果您在过程S中分配了一些内存,并将其放在堆栈上,然后退出S,那么分配的数据将从堆栈中弹出。但是p中的变量x也指向该数据,所以x现在指向堆栈指针下的某个位置(假定堆栈向下增长),内容未知。如果堆栈指针只是向上移动而不清除其下的数据,则内容可能仍然存在,但是如果开始在堆栈上分配新数据,则指针X实际上可能指向该新数据。
不要过早地假设,因为其他应用程序代码和用法可能会影响您的函数。所以看功能是隔离是没有用的。
如果你对应用程序很认真,那么就用vtune-it或任何类似的分析工具来查看热点。
凯坦
我想说的是,实际上由gcc生成的代码(我还记得vs)没有进行堆栈分配的开销。
说到以下功能:
1 2 3 4 5 6 7 | int f(int i) { if (i > 0) { int array[1000]; } } |
下面是代码生成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | __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会变为另一个值。除非没有局部变量,否则只需要执行此指令。所以allocate局部变量没有开销。