繁体   English   中英

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

[英]Why is memory allocation on heap MUCH slower than on stack?

我已经被告知很多次了。 但我不知道为什么......从堆分配内存时涉及哪些额外成本? 和硬件有关吗? 它与CPU周期有关吗? 这么多的猜测,但没有确切的答案......有人可以给我一些详细说明吗?

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

添加 1 - 2022 年 5 月 13 日上午 10:42

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

而对于堆来说,它还涉及复杂的软件数据结构和算法,其中涉及函数调用(再次涉及堆栈)、内存访问等。

硬件速度很快,但不如软件灵活。

软件很灵活,但不如硬件快。

所以堆并不便宜。

因为堆是比栈复杂得多的数据结构。

对于许多体系结构,在堆栈上分配内存只是改变堆栈指针的问题,即它是一条指令。 在堆上分配内存包括寻找一个足够大的块,将其拆分,并管理允许诸如free()之类的东西以不同顺序执行的“簿记”。

当范围(通常是函数)退出时,保证在堆栈上分配的内存被释放,并且不可能只释放其中的一部分。

在您重申展开的答案的编辑中,您提到了“堆数据结构”。 要非常小心,因为称为的数据结构与动态内存分配无关。 为了清楚起见,我将使用free store的更多语言律师术语。

正如已经指出的那样,堆栈分配需要增加一个指针,该指针通常在大多数架构上都有一个专用寄存器,而解除分配需要相同的工作量。 堆栈分配也适用于特定功能。 这使它们成为编译器优化的更好候选者,例如预先计算堆栈所需的总空间并为整个堆栈帧执行单个增量。 同样,堆栈具有更好的数据局部性保证。 堆栈的顶部几乎总是保证在高速缓存行内,正如我已经提到的,堆栈指针通常存储在寄存器中。 在某些架构上优化编译器甚至可以通过重用来自先前堆栈帧的参数来完全消除堆栈上的分配,这些参数作为参数传递给更深的堆栈帧中的被调用函数。 同样,堆栈分配的变量通常也可以提升为寄存器以避免分配。

相比之下,免费商店要复杂得多 我什至不打算开始讨论垃圾收集系统,因为那是一个完全不同的话题,而这个问题是关于 C 语言的。 通常,来自空闲存储的分配和释放涉及几种不同的数据结构,例如空闲列表或块池。 这些数据结构和簿记也需要内存,因此浪费了空间。 此外,簿记记录经常与分配混合,从而损害其他分配的数据局部性。 来自空闲存储的分配可能涉及向底层操作系统请求更多的进程内存,通常来自某种形式的slab分配器。

为了进行简单的比较,并使用 jemalloc-2.2.5 和 sloccount 中的数字作为参考,jemalloc 实现包含超过 8,800 行 C 语言源代码和另外 700 多行测试代码。 这应该让您很好地了解自由存储分配和堆栈分配之间的复杂性差异:数千行 C 代码与一条指令。

此外,由于免费存储分配不限于单个词法范围,因此必须跟踪每个分配的生命周期。 同样,这些分配可能会跨线程传递,因此线程同步问题会进入问题空间。 免费存储分配的另一个大问题是碎片化。 碎片化会导致很多问题:

  • 碎片化会损害数据的局部性。
  • 碎片化浪费内存。
  • 碎片化使得为大量分配寻找可用空间的工作变得更加困难。

在现代系统中,与自由存储相比,堆栈通常相对较小,因此自由存储最终会管理更多空间,从而解决更难的问题。 此外,由于堆栈大小的限制,空闲存储通常用于较大的分配,必须处理非常大和非常小的分配之间的这种差异也使得空闲存储的工作更加困难。 通常堆栈分配很小,大约为几千字节或更少,堆栈的总大小只有几兆字节。 自由存储区通常被赋予程序中所有剩余的进程空间 在现代机器上,这可能是几百 GB,并且免费存储分配的大小从几个字节(如短字符串)到 MB 甚至 GB 的任意数据不等,这种情况并不少见。 这意味着自由存储分配器必须处理底层操作系统的虚拟内存管理。 堆栈分配本质上是计算机硬件内置的。

如果您想真正了解免费存储分配,我强烈建议您阅读发表的有关各种 malloc 实现的许多论文和文章中的一些,甚至阅读代码。 以下是一些帮助您入门的链接:

  • dlmalloc -Doug Lea 的 malloc,在某个时间点用于 GNU C++ 的历史参考 malloc 实现
  • phkmalloc - 由 Varnish Web 缓存的 Poul-Henning Kamp 编写的 malloc 的 FreeBSD 实现
  • tcmalloc - 一些 Google 开发人员实现的线程缓存 Malloc
  • jemalloc - Jason Evan 的 FreeBSD 的 malloc 实现(phkmalloc 的继任者)

以下是一些附加链接,其中包含 tcmalloc 实现的描述:

堆栈和堆之间的主要区别在于堆栈上的项目不能乱序删除。 如果您将项目 A、B、C 添加到堆栈中,则必须先移除 C,否则无法移除 B。 这意味着向堆栈添加新项总是意味着将其添加到堆栈的末尾,这是一个非常简单的操作。 您只需移动指向堆栈末尾的指针。

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

在堆栈区域创建数据:只需移动堆栈指针在头部区域创建数据:在内存池上搜索满足给定要求的区域(您可以使用第一次拟合,最佳拟合或最差拟合)。找到区域后存储资料(簿记)

堆栈删除:堆栈删除很容易。只需将堆栈指针向下移动堆区域删除:查找元素在堆上的存储位置(使用簿记)并在必要时合并两个相邻的空闲块;

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM