哪个更快:堆栈分配还是堆分配

这个问题可能听起来相当简单,但这是我与另一位开发人员的争论。

我小心地在我可以的地方进行堆栈分配,而不是堆分配。他和我说话,看着我的肩膀,并评论说,这是没有必要的,因为他们的表现是一样的。

在我的印象中,增加堆栈是一个常数时间,而堆分配的性能取决于当前堆的复杂性,包括分配(找到合适大小的孔)和反分配(缩小孔以减少碎片,如果我没有弄错的话,许多标准库实现在删除过程中需要时间来做这件事)。

在我看来,这可能非常依赖于编译器。对于这个项目,我特别使用PPC架构的Metrowerks编译器。对这种组合的深入了解将是最有帮助的,但一般来说,对于GCC和msvc++,情况如何?堆分配不如堆栈分配高效吗?没有区别吗?还是差异如此之小以至于变成了毫无意义的微观优化。

134394 次浏览

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

我也同意Torbjörn Gyllebring关于对象的预期生命期的观点。好点!

堆栈分配要快得多,因为它真正做的只是移动堆栈指针。 使用内存池,您可以从堆分配中获得类似的性能,但这会略微增加复杂性,并带来令人头痛的问题。< / p >

此外,堆栈与堆不仅是性能方面的考虑;它还告诉您许多关于对象的预期生存期的信息。

我不认为堆栈分配和堆分配通常是可以互换的。我也希望它们的性能都足以用于一般用途。

我强烈推荐小件物品,哪种更适合分配范围。对于较大的项,堆可能是必要的。

在有多个线程的32位操作系统上,堆栈通常是相当有限的(尽管通常至少是几mb),因为需要分割地址空间,迟早一个线程堆栈会碰到另一个线程堆栈。在单线程系统(至少是Linux glibc单线程)上,限制要小得多,因为堆栈可以不断增长。

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

堆栈要快得多。它在大多数架构上只使用一条指令,在大多数情况下,例如在x86上:

sub esp, 0x10

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

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

同样,没有什么理由去优化那些不需要它的代码的性能,比如通过分析来证明。“过早的优化”通常会导致比它本身价值更多的问题。

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

通常,堆栈分配只是由堆栈指针寄存器中的减法组成。这比搜索堆快多了。

有时,堆栈分配需要添加一页(多个)虚拟内存。添加一个内存为零的新页不需要从磁盘读取一个页,所以通常这仍然比搜索堆要快得多(特别是当堆的一部分也被换出时)。在一种罕见的情况下(您可以构建这样一个示例),在RAM中已经存在的堆的一部分恰好有足够的可用空间,但是为堆栈分配一个新页必须等待其他一些页被写入磁盘。在这种罕见的情况下,堆会更快。

我认为生命期很重要,被分配的东西是否必须以复杂的方式构造。例如,在事务驱动的建模中,您通常必须填写并将带有一堆字段的事务结构传递给操作函数。以OSCI SystemC TLM-2.0标准为例。

在栈上靠近操作调用的地方分配这些资源往往会导致巨大的开销,因为这种构造非常昂贵。好的方法是在堆上分配和重用事务对象,或者通过池或简单的策略,如“这个模块只需要一个事务对象”。

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

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

我会说:两种都试试,看看哪种最适合你,因为这真的取决于你代码的行为。

可能堆分配和堆栈分配的最大问题是,堆分配在一般情况下是一个无界操作,因此在有时间问题的地方不能使用它。

对于时间不是问题的其他应用程序,它可能没有那么重要,但如果您分配了很多堆,这将影响执行速度。总是尝试将堆栈用于短期和经常分配的内存(例如在循环中),并尽可能长时间地在应用程序启动期间进行堆分配。

堆栈的容量有限,而堆则不是。一个进程或线程的典型堆栈大约是8K。一旦分配,就不能更改大小。

堆栈变量遵循作用域规则,而堆变量则不遵循。如果你的指令指针超出了一个函数,所有与该函数相关的新变量都会消失。

最重要的是,您无法预先预测整个函数调用链。因此,仅200字节的分配就可能导致堆栈溢出。如果您正在编写一个库,而不是应用程序,这一点尤其重要。

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

#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个时钟滴答”,即使这样,对于堆栈分配,我也得到“0个时钟滴答”,对于堆分配,我得到“15个时钟滴答”。当我测量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

这不仅仅是堆栈分配更快。您还可以在使用堆栈变量方面获得很多好处。它们有更好的参考位置。最后,折价也便宜得多。

关于这种优化有一个普遍的观点。

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

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

只有当你发现在对象的堆分配上花费了大量时间时,才会明显地更快地进行堆栈分配。

不要做过早的假设,因为其他应用程序代码和使用可能会影响您的功能。因此,孤立地看待函数是没有用的。

如果你是认真的应用程序,那么VTune它或使用任何类似的分析工具,并查看热点。

Ketan

前面提到过,堆栈分配只是移动堆栈指针,即大多数架构上的一条指令。将其与一般在堆分配情况下发生的情况进行比较。

操作系统以链表的形式维护部分空闲内存,有效负载数据由指向空闲部分起始地址的指针和空闲部分的大小组成。为了分配X字节的内存,将遍历链表,并按顺序访问每个音符,检查其大小是否至少为X。当找到大小为P >= X的部分时,将P分成大小为X和P-X的两个部分。更新链表并返回指向第一部分的指针。

如您所见,堆分配取决于许多因素,如您请求的内存大小、内存的碎片程度等等。

一般来说,正如上面几乎每个答案所提到的,堆栈分配比堆分配快。堆栈的push或pop是O(1),而从堆中分配或释放可能需要遍历之前的分配。但是,您通常不应该在紧凑的性能密集型循环中进行分配,因此选择通常取决于其他因素。

做出这样的区分可能会有好处:您可以在堆上使用“堆栈分配器”。严格地说,我认为堆栈分配是指分配的实际方法,而不是分配的位置。如果你在实际的程序堆栈上分配了很多东西,这可能会因为各种各样的原因而变得很糟糕。另一方面,在可能的情况下使用堆栈方法在堆上进行分配是分配方法的最佳选择。

既然你提到了《Metrowerks》和《PPC》,我猜你指的是Wii。在这种情况下,内存是非常宝贵的,在任何可能的情况下使用堆栈分配方法都可以保证您不会在片段上浪费内存。当然,这样做需要比“普通”堆分配方法更加小心。对每种情况进行权衡是明智的。

关于Xbox 360 Xenon处理器上的堆栈与堆分配,我了解到一件有趣的事情,这可能也适用于其他多核系统,即在堆上分配会导致进入临界区以停止所有其他核,这样分配就不会发生冲突。因此,在一个紧密循环中,堆栈分配是固定大小数组的方法,因为它可以防止停顿。

如果您正在为多核/多进程编码,这可能是另一个需要考虑的加速,因为您的堆栈分配将只由运行您的作用域函数的核心可见,而不会影响任何其他内核/ cpu。

除了与堆分配相比具有数量级的性能优势外,堆栈分配对于长时间运行的服务器应用程序更可取。即使是管理得最好的堆最终也会碎片化,导致应用程序性能下降。

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

然而,在处理基于堆栈和基于堆的分配的总体性能时(或者更好地说,本地和外部分配),存在更大的问题。通常,堆(外部)分配很慢,因为它要处理许多不同类型的分配和分配模式。减少您正在使用的分配器的范围(使其在算法/代码的本地)将倾向于在不做任何重大更改的情况下提高性能。为分配模式添加更好的结构,例如,通过以更简单和更结构化的方式使用分配器,强制对分配和回收对进行后进先出排序也可以提高分配器的性能。或者,您可以使用或编写针对特定分配模式调优的分配器;大多数程序经常分配一些离散的大小,因此基于几个固定(最好是已知)大小的备用缓冲区的堆将执行得非常好。Windows使用它的低碎片堆正是出于这个原因。

另一方面,如果线程太多,在32位内存范围上基于堆栈的分配也充满了危险。堆栈需要一个连续的内存范围,因此线程越多,就需要更多的虚拟地址空间来让它们在没有堆栈溢出的情况下运行。对于64位的程序来说,这(目前)不是问题,但是对于具有大量线程的长时间运行的程序来说,它肯定会造成严重破坏。由于碎片化而导致虚拟地址空间耗尽总是一件令人痛苦的事情。

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

通常,如果关心性能,堆分配是最后的选择。一个可行的中间选项可以是一个固定池分配器,它也只有几个指令,每次分配开销很小,所以它非常适合固定大小的小对象。缺点是它只适用于固定大小的对象,本质上不是线程安全的,并且有块碎片问题。

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

但是,如果复制对象的代价很高,那么如果不小心,在堆栈上分配可能会导致以后使用对象时的巨大性能损失。

例如,如果你在堆栈上分配了一些东西,然后将其放入容器中,那么在堆上分配并将指针存储在容器中会更好(例如使用std::shared_ptr<>)。同样的情况也适用于按值传递或返回对象,以及其他类似的情况。

重点是,尽管在许多情况下堆栈分配通常比堆分配更好,但有时如果你在不最适合计算模型的情况下费尽脑汁进行堆栈分配,它可能会导致比它解决的问题更多的问题。

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

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

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

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

我想说,实际上代码生成的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会改变为另一个值。除非你没有局部变量,否则这条指令只需要执行。所以分配局部变量没有开销。

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中是这样的。当你在func中,f1和指针f2已经被分配到堆栈上(自动存储)。顺便说一下,Foo f1(a1)在堆栈指针(esp)上没有指令效果,它已经被分配,如果func想要获得成员f1,它的指令是这样的:lea ecx [ebp+f1], call Foo::SomeFunc()。堆栈分配可能会让人认为内存是FIFO这样的东西,FIFO只是在你进入某个函数时发生的,如果你在函数中分配像f10这样的东西,就没有推送发生。

c++语言特有的关注点

首先,没有所谓的“堆栈”;或“;heap"c++规定的分配。如果你谈论的是块作用域中的自动对象,它们甚至没有被“分配”。(顺便说一下,C语言中的自动存储时间肯定与“分配”不一样;;后者是“动态的”;用c++的说法。)动态分配的内存在免费存储上,而不一定在“堆”上,尽管后者通常是(默认的)实现

尽管根据抽象机器语义规则,自动对象仍然占用内存,但当能够证明这无关紧要时(当它不会改变程序的可观察行为时),兼容的c++实现可以忽略这一事实。这个权限是由ISO c++中的假设规则授予的,这也是支持通常优化的通用子句(ISO C中也有几乎相同的规则)。除了as-if规则,ISO c++中还有复制省略规则来允许省略特定的对象创建。因此省略了所涉及的构造函数和析构函数调用。因此,与源代码中隐含的简单抽象语义相比,这些构造函数和析构函数中的自动对象(如果有的话)也被消除了。

另一方面,免费商店的分配肯定是“分配”;通过设计。在ISO c++规则下,这样的分配可以通过调用分配功能来实现。然而,自ISO c++ 14以来,有一个新的(非假定)规则来允许在特定情况下合并全局分配函数(即::operator new)调用。因此,部分动态分配操作也可以像自动对象一样是无操作的。

分配函数用于分配内存资源。可以使用分配器根据分配进一步分配对象。对于自动对象,它们是直接呈现的——尽管底层内存可以被访问,并被用来为其他对象提供内存(通过放置new),但这作为免费存储没有太大意义,因为没有办法将资源移动到其他地方。

所有其他问题都超出了c++的范围。尽管如此,它们仍然是重要的。

c++的实现

c++不公开具体化的激活记录或某些类型的一级延续(例如著名的call/cc),没有办法直接操作激活记录帧-实现需要将自动对象放置到的地方。一旦与底层实现没有(不可移植的)互操作("native"不可移植的代码,如内联汇编代码),帧的底层分配的遗漏可能是相当微不足道的。例如,当被调用的函数是内联的,帧可以有效地合并到其他帧中,因此没有办法显示什么是“分配”。

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

因为大多数c++实现(特别是那些针对isa级别的本机代码并使用汇编语言作为其直接输出的实现)使用类似的策略,这样一个令人困惑的“分配”;方案很受欢迎。这样的分配(以及释放)确实会消耗机器周期,而且当(非优化的)调用频繁发生时,代价会很高,即使现代CPU微架构可以通过硬件为公共代码模式实现复杂的优化(比如在实现PUSH/POP指令时使用堆栈引擎)。

但无论如何,一般来说,的确,栈帧分配的开销比调用一个操作free store的分配函数要小得多(除非它被完全优化了)。本身可以有数百个(如果不是数百万个:-)操作来维护堆栈指针和其他状态。分配函数通常基于托管环境提供的API(例如,操作系统提供的运行时)。与为函数调用保存自动对象的目的不同,这样的分配是通用的,所以它们不会有像堆栈那样的框架结构。传统上,它们从称为(或几个堆)的池存储中分配空间。不同于“叠”,“堆”的概念;这里没有说明使用的数据结构;# EYZ2。(顺便说一下,调用堆栈通常在程序/线程启动时由环境从堆中分配固定或用户指定的大小。)用例的本质使得从堆中分配和释放要复杂得多(比推送/弹出堆栈帧),并且几乎不可能由硬件直接优化。

对内存访问的影响

通常的堆栈分配总是把新帧放在最上面,所以它有很好的局部性。这对缓存是友好的。OTOH,在自由存储区中随机分配的内存没有这样的属性。从ISO c++ 17开始,就有了<memory_resource>提供的池资源模板。这种接口的直接目的是允许连续分配的结果在内存中接近。这承认了这样一个事实,即这种策略在现代实现中通常对性能很好,例如在现代架构中对缓存很友好。不过,这是关于访问而不是分配的性能。

并发性

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

空间效率

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

堆栈分配的限制

尽管在现实中,堆栈分配在性能上通常优于堆分配,但这并不意味着堆栈分配总是可以取代堆分配。

首先,在ISO c++中无法以可移植的方式在运行时指定大小的堆栈上分配空间。alloca和g++的VLA(变长数组)等实现提供了扩展,但是有理由避免使用它们。(IIRC, Linux源代码最近删除了VLA的使用)(还要注意ISO C99确实有强制的VLA,但ISO C11是可选的支持。)

其次,没有可靠和可移植的方法来检测堆栈空间耗尽。这通常被称为堆栈溢出(嗯,这个网站的词源),但可能更准确地说是堆栈溢出。在现实中,这通常会导致无效的内存访问,然后程序的状态被破坏(…或者更糟,可能是一个安全漏洞)。事实上,ISO c++没有“堆栈”的概念。和# EYZ2。注意应该为自动对象留出多少空间。

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

然而,有时需要深度递归调用。在需要支持未绑定活动调用的语言实现中(其中调用深度仅受总内存限制),像典型的c++实现一样,使用不可能的直接使用(当代)本机调用堆栈作为目标语言激活记录。为了解决这个问题,需要构造激活记录的替代方法。例如,SML /新泽西显式地在堆上分配帧,并使用仙人掌栈。这种激活记录帧的复杂分配通常不如调用堆栈帧快。然而,如果这些语言在适当尾递归的保证下进一步实现,则对象语言中的直接堆栈分配(即"object"在语言中不存储为引用,但本机原语值可以一对一映射到非共享的c++对象)甚至更复杂,通常性能更差。当使用c++实现这些语言时,很难估计性能影响。

自然,堆栈分配更快。使用堆分配,分配器必须在某处找到空闲内存。使用堆栈分配,编译器只需要给你的函数一个更大的堆栈框架就可以完成,这意味着分配完全不需要花费时间。(我假设你没有使用alloca或任何东西来分配动态的堆栈空间,但即使这样,它也非常快。)

但是,您必须警惕隐藏的动态分配。例如:

void some_func()
{
std::vector<int> my_vector(0x1000);
// Do stuff with the vector...
}

您可能认为这会在堆栈上分配4 KiB,但您错了。它在堆栈上分配了vector实例,但是这个vector实例又在堆上分配了它的4 KiB,因为vector 总是在堆上分配了它的内部数组(至少除非您指定了一个自定义分配器,这里我不会详细介绍)。如果你想使用类似stl的容器在堆栈上进行分配,你可能需要std::array,或者boost::static_vector(由外部的提高库提供)。