【问题标题】:What data structure is used to implement the dynamic memory allocation heap?使用什么数据结构来实现动态内存分配堆?
【发布时间】:2012-11-26 22:00:45
【问题描述】:

我一直认为heap (data structure) 用于实现heap (dynamic memory allocation),但有人告诉我我错了。

堆(例如,典型的malloc 例程或Windows 的HeapCreate 等实现的堆)通常是如何实现的?他们使用什么数据结构?

不是在问什么:

在网上搜索时,我看到了关于如何实现堆有严格限制的描述。
仅举几例,我已经看到很多关于如何实现的描述:

  • 永远不会将内存释放回操作系统的堆 (!)
  • 仅在类似大小的小块上提供合理性能的堆
  • 只能为大的连续块提供合理性能的堆

有趣的是,他们都回避了更难的问题:
“正常”的通用堆(如 mallocHeapCreate 后面的堆)是如何实现的?

他们使用什么数据结构(也许还有算法)?

【问题讨论】:

  • 是的,这两种堆是不同的。要了解 dlmalloc(由 glibc 使用)或 tcmalloc(由 google 使用)的动态内存分配 google。
  • @brianbeuning:会看看 dlmalloc,谢谢。但是TCMalloc currently does not return any memory to the system.
  • @Mehrdad:是的。大多数(全部?)基于 Unix 的 malloc 不会将内存返回给系统。
  • 我认为 C++ 和 C 标记在这里不合适,但我想不出更好的标记。
  • 最近版本的 dlmalloc 有一个很酷的特性,叫做 mspaces。您可以在 mspace 上使用 malloc() 和 free(),或者您可以删除 mspace 并释放 mspace 中分配的所有内存。我们在我们的应用程序服务器中使用它,因此每个 Web 会话都有自己的 mspace。 mspace 极大地改善了页面和缓存的局部性,如果我们的代码有任何内存泄漏错误,删除 mspace 可以修复泄漏。我们的会话使用一个线程,因此 mspaces 可以帮助解决最近分配器解决的多线程问题。

标签: data-structures memory-management heap-memory


【解决方案1】:

分配器往往非常复杂,并且在实现方式上往往存在很大差异。

你不能用一种常见的数据结构或算法来描述它们,但有一些共同的主题:

  1. 内存是以大块的形式从系统中获取的——通常一次是兆字节。
  2. 然后在您执行分配时,这些块会被拆分为多个较小的块。与您分配的大小不完全相同,但通常在特定范围内(200-250 字节、251-500 字节等)。有时这是多层的,在实际请求之前,您会有额外的“中等块”层。
  3. 控制从哪个“大块”中分离一块是一件非常困难且重要的事情 - 这会极大地影响内存碎片。
  4. 为每个范围维护一个或多个空闲池(又名“空闲列表”、“内存池”、“后备列表”)。有时甚至是线程本地池。这可以大大加快分配/释放许多类似大小对象的模式。
  5. 大分配的处理方式略有不同,以免浪费大量 RAM,也不会被池化(如果有的话)。

如果您想查看一些源代码,jemalloc 是一种现代高性能分配器,应该在其他常见分配器的复杂性方面具有代表性。 TCMalloc 是另一个常见的通用分配器,他们的网站介绍了所有血腥的实现细节。 Intel 的Thread Building Blocks 有一个专为高并发构建的分配器。

在 Windows 和 *nix 之间可以看到一个有趣的区别。在 *nix 中,分配器对应用程序使用的地址空间具有非常低级的控制。在 Windows 中,您基本上有一个粗粒度的慢速分配器 VirtualAlloc 来作为您自己的分配器的基础。

这会导致 *nix 兼容的分配器通常直接为您提供 malloc/free 实现,假设您只会对所有事情使用一个分配器(否则它们会互相践踏),而 Windows-特定的分配器提供额外的功能,让malloc/free 单独使用,并且可以协调使用(例如,您可以使用 HeapCreate 来创建可以与其他人一起工作的私有堆)。

在实践中,这种灵活性交易让 *nix 分配器在性能方面略有提升。很少看到应用程序在 Windows 上故意使用多个堆 - 主要是由于不同的 DLL 使用不同的运行时,每个 DLL 都有自己的malloc/free,如果你是不勤于跟踪一些内存来自哪个堆。

【讨论】:

  • 好的,所以(总而言之)他们维护不同大小的池,而不是让所有东西都变大小......有趣,谢谢。 +1
  • 我不明白的一件事:VirtualAlloc 如何比任何 *nix 系统使用的更粗粒度?
  • 好吧,我并没有说它是 more 粗粒度的,但实际上——VirtualAlloc 强制你分配 64KB(现在..这可能会在未来发生变化)块,而我相信 sbrkmmap 都在(通常)4KB 页面上运行。
【解决方案2】:

注意:以下答案假设您使用的是具有虚拟内存的典型现代系统。 C 和 C++ 标准不需要虚拟内存;因此,您当然不能在没有此功能的硬件上依赖此类假设(例如,GPU 通常没有此功能;像 PIC 这样的极小的硬件也没有)。


这取决于您使用的平台。堆可以是非常复杂的野兽;他们不只使用单一的数据结构;并且没有“标准”数据结构。即使堆代码所在的位置也因平台而异。例如,堆代码通常由 C Runtime on Unix 机器提供;但通常由 Windows 上的操作系统提供。

  1. 是的,这在 Unix 机器上很常见;由于 *nix 的底层 API 和内存模型的运行方式。基本上,在这些系统上将内存返回给操作系统的标准 API 只允许在分配用户内存的位置与用户内存和系统设施(如堆栈)之间的“洞”之间的“边界”上返回内存。 (有问题的 API 是 brk or sbrk)。许多堆并没有将内存返回给操作系统,而是仅尝试重用程序不再使用的内存,而不尝试将内存返回给系统。这在 Windows 上不太常见,因为它等同于 sbrk (VirtualAlloc) 没有此限制。 (但就像sbrk 一样,它非常昂贵,并且有一些警告,比如只分配页面大小和页面对齐的块。所以堆尝试尽可能少地调用其中任何一个)
  2. 这听起来像一个“块分配器”,它将内存分成固定大小的块,然后只返回一个空闲块。据我(尽管有限)理解,Windows 的RtlHeap 为不同的已知块大小维护了许多这样的数据结构。 (例如,它将有一个用于大小为 16 的块)RtlHeap 将这些称为“后备列表”。
  3. 我真的不知道能很好地处理这种情况的特定结构。对于大多数分配系统来说,大块是有问题的,因为它们会导致地址空间碎片化。

我发现讨论主要平台上采用的常见分配策略的最佳参考是《Secure Coding in C and C++, by Robert Seacord》一书。第 4 章的全部内容都专门讨论堆数据结构(以及用户错误使用所述堆系统所导致的问题)。

【讨论】:

  • 我真的不明白你在说sbrk 无法释放内存等等……假设的程序与这个问题有什么关系?跨度>
  • @Mehrdad:基本上,*nix 使用的模型是用户动态内存全部分配在“数据段”中。然后有一个未使用的内存“洞”。然后最重要的是堆栈。 (或多或少——哪条路向上,哪条路向下取决于平台约定;为操作系统保留一些内存空间;等等。)在*nix系统上请求内存的方式是调用sbrk ,这只会将数据段大小增加到用户数据和堆栈等之间的“未使用的洞”中。
  • @Mehrdad:所以,是的,堆算法可以缩小所述数据段的大小。但是它可以这样做的情况很少见,并且在free 中检测 和当前数据段末尾之间的所有内存都未被使用的情况将非常昂贵。所以大多数(全部?)分配器不这样做。
  • 对了,然后你可以把数据段的大小减小到同样的程度,那么问题到底出在哪里呢?这不是在实践中完成的吗? (如果是这样,那意味着如果我打开像 top 这样的实用程序,我应该永远不会看到内存使用量下降,对吧?)
  • @Mehrdad:例如,您有一个使用sbrk 分配大小为100MB 的内存的程序。然后用sbrk 分配大小为1MB 的内存。然后释放大小 100MB。没有办法将其返回系统;因为那 1MB 超出了保持数据段的大小。没有办法在中间返回那个记忆。由于这是一个非常常见的问题,并且由于检测数据段的整个末端何时空闲是昂贵的,大多数mallocs 不这样做。
猜你喜欢
  • 2020-10-14
  • 2012-11-07
  • 1970-01-01
  • 1970-01-01
  • 2016-04-10
  • 2012-03-12
  • 1970-01-01
  • 2020-04-18
相关资源
最近更新 更多