这个问题可能听起来相当简单,但这是我与另一位与我合作的开发人员的辩论。
我正在小心处理堆栈分配的东西,而不是堆分配它们。他正在跟我说话并且看着我的肩膀并评论说没有必要,因为他们的表现是明智的。
我一直认为堆栈的增长是恒定的时间,并且堆分配的性能取决于堆的当前复杂性(用于找到合适大小的孔)和解除分配(折叠孔以减少)碎片化,因为许多标准库实现在删除期间需要时间来执行此操作,如果我没有记错的话。)
这让我觉得可能非常依赖于编译器。特别是对于这个项目,我正在为Metrowerks架构使用PPC编译器。对这种组合的洞察力将是最有帮助的,但总的来说,对于GCC和MSVC ++,情况如何?堆分配不如堆栈分配高吗?没有区别吗?或者差异是如此微小,它变得毫无意义的微优化。
答案 0 :(得分:471)
堆栈分配要快得多,因为它实际上只是移动堆栈指针。 使用内存池,您可以从堆分配中获得可比较的性能,但这会带来轻微的复杂性和自身的麻烦。
此外,堆栈与堆不仅是性能考虑因素;它还告诉你很多关于对象的预期寿命。
答案 1 :(得分:163)
堆栈速度更快。它实际上只在大多数架构上使用单个指令,在大多数情况下,例如,在x86上:
sub esp, 0x10
(它将堆栈指针向下移动0x10字节,从而“分配”这些字节以供变量使用。)
当然,堆栈的大小非常非常有限,因为您会很快发现是否过度使用堆栈分配或尝试进行递归: - )
此外,没有理由优化不可验证的代码性能,例如分析所证明的。 “过早优化”通常会导致更多问题,而不是它的价值。
我的经验法则:如果我知道我在编译时需要一些数据 ,而且它的大小只有几百个字节,我会叠加分配它。否则我会堆分配它。
答案 2 :(得分:115)
答案 3 :(得分:29)
我在Xbox 360 Xenon处理器上了解堆栈与堆分配的一个有趣的事情,也可能适用于其他多核系统,是在堆上分配导致关键部分被输入以停止所有其他核心,以便alloc不冲突。因此,在紧密循环中,Stack Allocation是固定大小数组的方法,因为它可以防止停顿。
如果您正在为多核/多进程编码,这可能是另一个加速考虑,因为您的堆栈分配只能由运行您的作用域函数的核心查看,并且不会影响任何其他核心/ CPU。
答案 4 :(得分:18)
您可以为特定大小的对象编写特殊的堆分配器,这些对象非常高效。但是,通用堆分配器不是特别有效。
我同意TorbjörnGyllebring关于物体的预期寿命。好点!
答案 5 :(得分:7)
我认为堆栈分配和堆分配通常不可互换。我也希望它们的性能足以满足一般用途。
我强烈推荐小件物品,无论哪个更适合分配范围。对于大型项目,堆可能是必要的。
在具有多个线程的32位操作系统上,堆栈通常相当有限(尽管通常至少为几mb),因为地址空间需要被分割,迟早会有一个线程堆栈运行到另一个。在单线程系统(无论如何都是Linux glibc单线程)上,限制要少得多,因为堆栈可以增长和增长。
在64位操作系统上,有足够的地址空间可以使线程堆栈非常大。
答案 6 :(得分:6)
通常堆栈分配只是从堆栈指针寄存器中减去。这比搜索堆快得多。
有时堆栈分配需要添加一个或多个虚拟内存页面。添加新的归零内存页面不需要从磁盘读取页面,因此通常这比搜索堆快得多(特别是如果堆的一部分也被分页)。在极少数情况下,你可以构造这样一个例子,在堆已经在RAM中的部分堆中恰好可以使用足够的空间,但是为堆栈分配新页面必须等待其他页面被写出来到磁盘。在这种罕见的情况下,堆更快。
答案 7 :(得分:6)
除了堆分配的数量级性能优势之外,堆栈分配对于长时间运行的服务器应用程序更为可取。即使是最好的托管堆最终也会变得如此分散,以至于应用程序性能会下降。
答案 8 :(得分:4)
堆栈的容量有限,而堆不是。进程或线程的典型堆栈大约为8K。分配后,您无法更改大小。
堆栈变量遵循范围规则,而堆栈变量不遵循范围规则。如果指令指针超出了函数,则与该函数关联的所有新变量都将消失。
最重要的是,您无法提前预测整个函数调用链。因此,只需200字节的分配可能会导致堆栈溢出。如果您正在编写库而不是应用程序,这一点尤其重要。
答案 9 :(得分:3)
这不仅仅是堆栈分配更快。你在使用堆栈变量方面也获得了很多。他们有更好的参考地点。最后,解除分配也便宜很多。
答案 10 :(得分:3)
堆栈分配是一对指令,而我所知的最快的rtos堆分配器(TLSF)平均使用150个指令。堆栈分配也不需要锁定,因为它们使用线程本地存储,这是另一个巨大的性能获胜。因此,堆栈分配可以快2-3个数量级,具体取决于您的环境多线程程度。
一般情况下,如果您关心性能,堆分配是最后的选择。一个可行的中间选项可以是一个固定的池分配器,它也只是一对指令,并且每个分配开销很少,因此对于小的固定大小的对象来说非常有用。在缺点方面,它只适用于固定大小的对象,本质上不是线程安全的,并且存在块碎片问题。
答案 11 :(得分:3)
堆栈分配几乎总是比堆分配快或快,尽管堆分配器当然可以简单地使用基于堆栈的分配技术。
但是,在处理堆栈与基于堆的分配的整体性能时(或稍微更好的术语,本地与外部分配)存在更大的问题。通常,堆(外部)分配很慢,因为它处理许多不同类型的分配和分配模式。减少正在使用的分配器的范围(使其成为算法/代码的本地)将倾向于在不进行任何重大更改的情况下提高性能。为分配模式添加更好的结构,例如,在分配和解除分配对上强制执行LIFO排序,也可以通过以更简单,更结构化的方式使用分配器来提高分配器的性能。或者,您可以使用或编写为您的特定分配模式调整的分配器;大多数程序经常分配一些离散大小,因此基于几个固定(最好是已知)大小的后备缓冲区的堆将表现得非常好。由于这个原因,Windows使用其低碎片堆。
另一方面,如果你有太多的线程,在32位内存范围上基于堆栈的分配也充满了危险。堆栈需要一个连续的内存范围,因此您拥有的线程越多,它们在没有堆栈溢出的情况下运行所需的虚拟地址空间就越多。这对于64位来说不会是一个问题(现在),但它肯定会在长时间运行的程序中造成严重破坏。由于碎片而耗尽虚拟地址空间总是很难处理。
答案 12 :(得分:3)
堆分配与堆栈分配的最大问题可能是一般情况下的堆分配是无限制的操作,因此在时间成为问题时你不能使用它。
对于时间不是问题的其他应用程序,它可能无关紧要,但如果堆分配很多,这将影响执行速度。总是尝试将堆栈用于短期和经常分配的内存(例如在循环中),并尽可能长时间 - 在应用程序启动期间进行堆分配。
答案 13 :(得分:3)
我认为生命是至关重要的,分配的东西是否必须以复杂的方式构建。例如,在事务驱动的建模中,您通常必须填充并将带有一堆字段的事务结构传递给操作函数。以OSCI SystemC TLM-2.0标准为例。
在靠近操作调用的堆栈上分配这些操作往往会导致巨大的开销,因为构造很昂贵。好的方法是在堆上分配并通过池化或简单的策略重用事务对象,例如“此模块只需要一个事务对象”。
这比在每个操作调用上分配对象快许多倍。
原因很简单,该物体具有昂贵的结构和相当长的使用寿命。
我会说:尝试两种方法,看看哪种情况最适合您的情况,因为它实际上取决于代码的行为。
答案 14 :(得分: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中会是这样的。当您在func
时,f1
和指针f2
已在堆栈上分配(自动存储)。顺便说一下,Foo f1(a1)
对堆栈指针(esp
)没有指令效果,它已被分配,如果func
想要得到成员f1
,它的指令是像这样:lea ecx [ebp+f1], call Foo::SomeFunc()
。堆栈分配的另一件事可能会让某人认为内存类似于FIFO
,FIFO
只是在你进入某个函数时发生,如果你在函数中并且分配了类似int i = 0
的内容,没有发生过推动。
答案 15 :(得分:2)
关于这种优化有一个普遍的观点。
您获得的优化与程序计数器实际位于该代码中的时间成正比。
如果您对程序计数器进行采样,您将找到它花费时间的位置,这通常只是代码的一小部分,并且通常在库例程中您无法控制。
只有当你发现在对象的堆分配中花费了很多时间时,堆栈分配它们才会明显加快。
答案 16 :(得分:2)
正如其他人所说,堆栈分配通常要快得多。
但是,如果您的对象复制起来很昂贵,那么在您使用这些对象时,如果您不小心,在堆栈上进行分配可能会导致巨大的性能损失。
例如,如果在堆栈上分配一些内容,然后将其放入容器中,最好在堆上进行分配并将指针存储在容器中(例如,使用std :: shared_ptr<> )。如果按值和其他类似方案传递或返回对象,情况也是如此。
关键在于,虽然在很多情况下堆栈分配通常比堆分配更好,但有时如果在最不适合计算模型的情况下你想要堆栈分配,它可能会导致比它更多的问题解决的问题。
答案 17 :(得分:1)
注意在选择堆栈与堆分配时,注意事项通常与速度和性能无关。堆栈就像一个堆栈,这意味着它非常适合推送块并再次弹出它们,最后输出。程序的执行也是堆栈式的,最后输入的程序首先退出。在大多数编程语言中,过程中所需的所有变量只能在过程执行期间可见,因此在进入过程时会被推送,并在退出或返回时弹出堆栈。
现在举例说明无法使用堆栈:
Proc P
{
pointer x;
Proc S
{
pointer y;
y = allocate_some_data();
x = y;
}
}
如果在过程S中分配一些内存并将其放在堆栈上然后退出S,则分配的数据将从堆栈中弹出。但是P中的变量x也指向该数据,因此x现在指向堆栈指针下面的某个位置(假设堆栈向下增长)具有未知内容。如果堆栈指针刚刚向上移动而不清除其下面的数据,则内容可能仍然存在,但如果您开始在堆栈上分配新数据,则指针x实际上可能指向该新数据。
答案 18 :(得分:1)
首先,没有C ++要求的所谓“堆栈”或“堆”分配。如果您在讨论块范围内的自动对象,它们甚至不会被“分配”。 (顺便说一句,在C中,自动存储的持续时间绝对不同于“分配”;在C ++中,后者是“动态的”。)动态分配的内存位于免费存储上,不一定在“堆”上,尽管后者通常是(默认)实现。
尽管按照抽象语义规则,自动对象仍会占用内存,但是当可以证明这无关紧要时(当它不改变程序的可观察行为时),允许遵循C ++的实现忽略此事实。此许可由ISO C ++中的as-if规则授予,这也是启用常规优化的常规子句(ISO C中也存在几乎相同的规则)。除了as-if规则外,ISO C ++还具有复制省略规则,以允许省略特定对象的创建。从而省略了所涉及的构造函数和析构函数调用。结果,与源代码所隐含的天真的抽象语义相比,这些构造函数和析构函数中的自动对象(如果有的话)也被消除了。
另一方面,免费商店的分配绝对是设计使然的“分配”。根据ISO C ++规则,可以通过调用分配函数来实现这种分配。但是,从ISO C ++ 14开始,在特定情况下有一个新的(非常规)规则允许合并全局分配函数(即::operator new
)调用。因此,动态分配操作的某些部分也可以像自动对象一样无操作。
分配功能分配内存资源。可以基于分配器使用分配器进一步分配对象。对于自动对象,它们是直接呈现的-尽管可以访问基础内存并用于为其他对象提供内存(通过放置new
),但这对于免费存储来说意义不大,因为存在无法将资源转移到其他地方。
所有其他问题不在C ++的范围之内。但是,它们仍然可能很重要。
C ++不会公开化的激活记录或某种一流的延续(例如,著名的call/cc
),无法直接操作激活记录帧-在实现中需要放置自动记录帧反对。一旦与基础实现(“本机”非便携式代码,例如内联汇编代码)不存在(非便携式)互操作,则忽略框架的基础分配可能就变得微不足道了。例如,当内联被调用函数时,框架可以有效地合并到其他框架中,因此无法显示什么是“分配”。
但是,一旦尊重互操作性,事情就会变得复杂。 C ++的典型实现将通过一些调用约定作为与本机(ISA级计算机)代码共享的二进制边界,来公开ISA(指令集体系结构)上的互操作能力。特别是在维护堆栈指针时,这显然是昂贵的,该指针通常由ISA级寄存器直接保存(可能需要访问特定的机器指令)。堆栈指针指示(当前活动的)函数调用的顶部帧的边界。输入函数调用时,需要一个新帧,并且堆栈指针的增加或减少(取决于ISA的约定)的值不得小于所需的帧大小。然后在操作后的堆栈指针被称为已分配。根据用于调用的调用约定,函数的参数也可以传递到堆栈框架上。框架可以保存由C ++源代码指定的自动对象(可能包括参数)的内存。从这种实现的意义上说,这些对象是“分配的”。当控件退出函数调用时,不再需要该帧,通常通过将堆栈指针恢复到调用之前的状态(根据调用约定先前保存的)来释放该帧。这可以视为“重新分配”。这些操作使激活记录有效地成为LIFO数据结构,因此通常称为“ the (call) stack”。堆栈指针有效地指示了堆栈的顶部位置。
因为大多数C ++实现(尤其是针对ISA级本机代码并使用汇编语言作为其直接输出的实现)都使用类似的策略,因此这种令人困惑的“分配”方案很受欢迎。这样的分配(以及取消分配)确实要花费机器周期,并且(非优化的)调用频繁发生时,即使现代的CPU微体系结构可以通过硬件为通用代码模式实现复杂的优化(例如使用堆栈引擎,用于实现PUSH
/ POP
指令)。
但是,总的来说,的确,堆栈帧分配的成本明显低于调用操作免费存储的分配函数的成本(除非已完全优化了)本身可以进行数百个(如果不是上百万个:-)操作来维护堆栈指针和其他状态。分配功能通常基于托管环境提供的API(例如,操作系统提供的运行时)。与为函数调用保留自动对象的目的不同,此类分配是通用的,因此它们不会像堆栈那样具有框架结构。传统上,它们从称为heap的池存储中分配空间(或几个堆)。与“堆栈”不同,此处的“堆”概念并不表示正在使用的数据结构。 it is derived from early language implementations decades ago。 (顺便说一句,在程序或线程启动时,调用栈通常由环境从堆中分配固定大小或用户指定的大小。)用例的性质使得从堆中进行分配和释放要复杂得多(比push或pop of堆栈帧),并且几乎不可能通过硬件直接进行优化。
通常的堆栈分配总是将新框架放在顶部,因此它具有很好的局部性。这很容易缓存。 OTOH,在免费存储区中随机分配的内存没有这种属性。从ISO C ++ 17开始,<memory>
提供了池资源模板。这种接口的直接目的是允许连续分配的结果在内存中紧密排列在一起。这承认了这样一个事实,即该策略通常对现代实现具有良好的性能,例如易于在现代架构中进行缓存。不过,这是关于访问的性能,而不是分配。
期望并发访问内存在堆栈和堆之间可能有不同的影响。调用堆栈通常由C ++实现中的一个执行线程专有。 OTOH,堆通常在进程中的线程之间共享。对于此类堆,分配和释放功能必须保护共享的内部管理数据结构免受数据争用。结果,由于内部同步操作,堆分配和释放可能会产生额外的开销。
由于用例和内部数据结构的性质,堆可能会受内部memory fragmentation的影响,而堆栈则不会。这对内存分配的性能没有直接影响,但是在具有virtual memory的系统中,低空间效率可能会降低内存访问的整体性能。当将HDD用作物理内存交换时,这尤其糟糕。它可能会导致相当长的延迟-有时数十亿个周期。
尽管实际上堆栈分配在性能上通常比堆分配要好,但这并不意味着堆栈分配总是可以代替堆分配。
首先,无法使用ISO C ++以可移植的方式在运行时在堆栈上分配具有指定大小的空间。 alloca
和G ++的VLA(可变长度数组)之类的实现提供了扩展,但是有避免使用它们的理由。 (IIRC,Linux源代码最近删除了对VLA的使用。)(还请注意,ISO C99确实具有VLA,但是ISO C11将支持变为可选。)
第二,没有可靠且可移植的方法来检测堆栈空间耗尽。这通常称为堆栈溢出(该站点的词源,嗯),但更准确地说,是“堆栈溢出”。实际上,这通常会导致无效的内存访问,然后破坏程序的状态(或更糟的是,出现安全漏洞)。实际上,ISO C ++没有堆栈和makes it undefined behavior when the resource is exhausted的概念。请注意应为自动对象留出多少空间。
如果堆栈空间用完,则堆栈中分配的对象过多,这可能是由于函数的活动调用过多或自动对象使用不当引起的。此类情况可能表明存在错误,例如没有正确退出条件的递归函数调用。
尽管如此,有时还是需要深度递归调用。在需要支持无界活动调用(调用深度仅受总内存限制)的语言的实现中,像典型的C ++实现一样,直接使用本机调用堆栈作为目标语言激活记录是不可能的。例如,SML/NJ在堆上显式分配帧并使用cactus stacks。这种激活记录帧的复杂分配通常不如调用堆栈帧快。但是,当使用proper tail recursion进一步实现语言时,以对象语言(即该语言中的“对象”)的直接堆栈分配未存储为引用,而是可以一对一映射到未共享的原始值C ++对象)甚至更复杂,并且通常会有更多的性能损失。使用C ++实现此类语言时,很难估计其性能影响。
答案 19 :(得分:1)
通常,堆栈分配比堆分配更快,正如上面几乎每个答案所提到的那样。堆栈推送或弹出是O(1),而从堆中分配或释放可能需要先行分配。但是,您通常不应该在紧密的,性能密集的循环中进行分配,因此选择通常会归结为其他因素。
进行这种区分可能会很好:您可以在堆上使用“堆栈分配器”。严格来说,我认为堆栈分配意味着实际的分配方法而不是分配的位置。如果你在实际的程序堆栈上分配了很多东西,那可能因为各种原因而变坏。另一方面,使用堆栈方法在可能的情况下在堆上进行分配是您可以为分配方法做出的最佳选择。
既然你提到了Metrowerks和PPC,我猜你的意思是Wii。在这种情况下,内存非常重要,并且尽可能使用堆栈分配方法保证您不会在内存上浪费内存。当然,这样做需要比“普通”堆分配方法更多的关注。评估每种情况的权衡是明智的。
答案 20 :(得分:1)
之前已经提到堆栈分配只是移动堆栈指针,即大多数架构上的单个指令。将其与堆分配情况下通常的情况进行比较。
操作系统将部分空闲存储器维护为链表,其中有效载荷数据包括指向空闲部分的起始地址和空闲部分的大小的指针。为了分配X字节的内存,遍历链接列表并按顺序访问每个音符,检查其大小是否至少为X.当找到大小为P> = X的部分时,P被分成两部分尺寸为X和PX。更新链接列表并返回指向第一部分的指针。
正如您所看到的,堆分配取决于可能的因素,例如您请求的内存量,内存的碎片程度等等。
答案 21 :(得分:0)
永远不要过早假设,因为其他应用程序代码和使用会影响您的功能。所以看功能是隔离是没有用的。
如果您认真对待应用程序,请将其VTune或使用任何类似的性能分析工具并查看热点。
科坦
答案 22 :(得分:-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:
因此,你有多少局部变量(即使在内部或切换),只有3880将变为另一个值。除非你没有局部变量,否则只需要执行该指令。所以分配局部变量没有开销。