为什么堆上的内存分配比栈上慢得多?

74

我听说过这个很多次,但是我不知道为什么...从堆中分配内存涉及哪些额外成本?这与硬件有关吗?与CPU周期有关吗?有很多猜测但没有确切的答案...能否有人给我一些解释呢?

就像“unwind”所说的,堆数据结构比栈更复杂。在我看来,当线程开始运行时,一些内存空间被分配到线程作为它的栈,而堆由进程内的所有线程共享。这种范式需要一些额外的机制来管理每个线程对共享堆的使用,例如垃圾回收。我理解得对吗?

ADD 1 - 10:42 AM 5/13/2022

栈的管理仅涉及指令和寄存器(SP,BP),从某种意义上说,这自然/纯粹是硬件。

而堆则涉及更复杂的软件数据结构和算法,包括函数调用(再次涉及栈)、内存访问等。

硬件快速但不如软件灵活。

软件灵活但不如硬件快速。

所以堆并不便宜。


1
请参考https://dev59.com/iHVC5IYBdhLWcg3w21Iq,该问题是关于C++的,但概念相同。 - kennytm
4个回答

71

由于堆栈比栈要复杂得多,因此在许多体系结构中,在堆栈上分配内存只涉及更改堆栈指针,即仅需一条指令。而在堆上分配内存则需要查找足够大的块、拆分它并管理“簿记”,以便以不同顺序执行free()等操作。

在堆栈上分配的内存保证在作用域退出时(通常是函数)进行释放,并且不能只释放其中的一部分。


9
最后一句话有些令人困惑。与其说“一次性全部丢失”,我会说它保证按照分配的相反顺序进行释放。 - Laurence Gonsalves

40
在您重新表述 unwind 的答案时提到了"堆数据结构"。请非常小心,因为被称为"堆"(heap)的数据结构与动态内存分配没有任何关系。为了非常清楚,我将使用更多的语言术语free store
正如已经指出的那样,栈分配需要递增一个指针,这个指针在大多数体系结构上都有专用寄存器,并且释放需要相同的工作。栈分配也仅限于特定函数的范围内。这使得它们更适合编译器优化,例如预计算栈中所需的总空间并对整个栈帧进行单个递增。同样,栈具有更好的数据局部性。栈顶几乎总是保证在缓存行内,而且如我之前提到的,栈指针通常存储在寄存器中。一些体系结构上的优化编译器甚至可以通过重用作为深层堆栈框架中调用函数的参数传递的前面堆栈框架中的参数来完全消除栈上的分配。同样,栈分配的变量通常可以升级为寄存器,避免分配。
相比之下,free store要复杂得多。我甚至不会开始涉及垃圾收集系统,因为这是一个完全不同的主题,而且这个问题是关于C语言的。通常,从自由存储区分配和释放涉及到几个不同的数据结构,如空闲列表或块池。这些数据结构和簿记也需要内存,因此该空间被浪费了。此外,簿记记录通常与分配混合在一起,因此会损害其他分配的数据局部性。来自自由存储区的分配可能涉及向底层操作系统请求更多进程内存,通常来自某种形式的板块分配器。
为了进行简单的比较,并使用jemalloc-2.2.5和sloccount的数字作为参考,jemalloc实现包含超过8,800行C语言源代码和另外700多行测试代码。这应该让您对自由存储区分配和栈分配之间的复杂性差异有一个很好的想法:数千行C代码与单个指令。此外,由于自由存储分配不仅限于单个词法作用域,因此必须跟踪每个分配的生命周期。同样,这些分配可能会在线程之间传递,因此线程同步问题将进入问题空间。对于自由存储分配来说,另一个很大的问题是碎片化。碎片化会引起许多问题:
  • 碎片化影响数据局部性。
  • 碎片化浪费内存。
  • 碎片化使得为大型分配查找可用空间的工作更加困难。

在现代系统中,相对于自由存储,堆栈通常比较小,因此最终自由存储管理更多的空间,从而解决了一个更难的问题。同时,由于堆栈大小的限制,自由存储通常用于大型分配,这种处理非常大和非常小的分配的差异也使自由存储的工作更加困难。通常,堆栈分配很小,只有几千字节或更少,堆栈的总大小仅为几兆字节。自由存储通常在程序中获得整个进程空间的剩余部分。在现代计算机上,这可以达到数百GB,自由存储分配的大小可以从几个字节(例如短字符串)到任意数据的MB甚至GB级别不等。这意味着自由存储分配器必须处理底层操作系统的虚拟内存管理。堆栈分配基本上是计算机硬件内置的。

如果您真的想了解自由存储分配,请强烈建议阅读一些关于malloc实现的研究论文和文章,甚至阅读代码。以下是一些入门链接:

  • dlmalloc - Doug Lea的malloc,GNU C ++曾在某个时间点使用的历史参考malloc实现
  • phkmalloc - Poul-Henning Kamp编写的FreeBSD malloc实现,Varnish Web Cache的作者
  • tcmalloc - 由一些Google开发人员实施的线程缓存分配器
  • jemalloc - Jason Evan为FreeBSD提供的malloc实现(phkmalloc的后继者)
  • 以下是一些关于tcmalloc实现的描述链接:


    17

    栈和堆的主要区别在于,栈中的项目不能按顺序移除。如果你向栈中添加了A、B、C项目,你就不能先移除B再移除C。这意味着将新项添加到栈中始终意味着将其添加到末尾,这是一个非常简单的操作。你只需要移动指向栈末尾的指针。

    另一方面,在堆上,你可以按任意顺序移除项目。只要之后不要在内存中移动其他项目(某些垃圾收集堆可能会这样做),那么堆中就有一个"空洞"。即,如果你将A、B、C添加到堆中,并删除B,则你的堆在内存中看起来像这样:A _ C,其中_是未使用(空闲)内存块。如果现在添加一个新项D,则分配器必须找到足够大的连续空闲空间来容纳D。这取决于内存中有多少连续的空闲空间,这可能是一项昂贵的操作。它几乎总是比仅移动栈的“最后元素”指针更昂贵。


    2

    在栈区创建数据:只需移动堆栈指针即可。 在头部区域创建数据:在内存池中搜索满足给定要求的区域(可以使用首次适配、最佳适配或最差适配)。找到区域后,存储信息(记账)。

    在栈上删除数据:删除栈上的数据很容易。只需向下移动堆栈指针即可。 在堆区删除数据:查找元素在堆上的存储位置(使用记账),如果需要,合并两个相邻的空闲块。


    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接