【问题标题】:Memory defragmentation/heap compaction - commonplace in managed languages, but not in C++. Why?内存碎片整理/堆压缩 - 在托管语言中很常见,但在 C++ 中不常见。为什么?
【发布时间】:2020-10-14 08:04:19
【问题描述】:

我一直在阅读有关托管语言的零暂停垃圾收集器的一些信息。据我了解,如果没有 stop-the-world 暂停,最困难的事情之一就是堆压缩。只有极少数的收藏家(例如 Azul C4、ZGC)似乎正在这样做,或者至少正在接近这一点。

因此,大多数 GC 引入了可怕的 stop-the-world 会暂停堆的压缩(糟糕!)。不这样做似乎非常困难,并且确实会带来性能/吞吐量损失。所以不管怎样,这一步似乎有点问题。

然而——据我所知,大多数(如果不是全部)GC 仍然偶尔会压缩堆。我还没有看到默认情况下不执行此操作的现代 GC。这让我相信:它必须非常非常重要。如果不是这样,当然,这种折衷是不值得的。

同时,我从未见过有人在 C++ 中进行内存碎片整理。我敢肯定有些人会这样做,但是-如果我错了,请纠正我-这似乎根本不是一个共同的问题。 我当然可以想象静态内存会在一定程度上减少这种情况,但可以肯定的是,大多数代码库都会进行相当多的动态分配?!

所以我很好奇,这是为什么呢?

我的假设(在托管语言中非常重要;在 C++ 中很少这样做)是否正确?如果是,我是否缺少任何解释?

【问题讨论】:

  • 你不能在 C++ 中进行堆压缩,因为这会改变人们存储指针的对象的地址。
  • 这很重要。长时间运行的 C++ 进程可能而且确实会遭受内存碎片的影响。现代内存分配器(如 tcmalloc 或 jemalloc)试图通过使用多个分配桶来处理相同大小的请求并管理一个空闲列表以确保在向操作系统请求额外内存之前首先填充这些桶中的漏洞,从而防止这些问题。托管语言有这样做的额外动力,因为您的 GC 在标记阶段必须扫描的内存页越少,它运行得越快。
  • C++使用操作系统内存分配系统(通过new/delete和malloc/free抽象出来)。如果操作系统使用虚拟内存,你真的无法压缩“堆”。原因是您所拥有的只是虚拟地址,而压缩虚拟内存可能不会像您期望的那样在物理内存中移动数据。
  • 如果您对这类内容感兴趣,请阅读jemalloc paper 或mimalloc paper 作为现代高级内存管理的介绍。
  • C/C++ 程序中使用的另一种技术:用于短期、突发分配的内存区域,可以一次性清理。这也可以防止内存碎片。

标签: c++ memory-management


【解决方案1】:

垃圾收集可以压缩堆,因为它知道所有指针的位置。毕竟,它刚刚完成了对他们的追踪。这意味着它可以移动对象并将指针(引用)调整到新位置。

但是,C++ 无法做到这一点,因为它不知道所有指针的位置。如果内存分配库移动了一些东西,可能会有指向旧位置的悬空指针。

哦,对于长时间运行的进程,C++ 确实会遭受内存碎片的影响。这在 32 位系统上更成问题,因为它可能无法从操作系统分配内存,因为它可能已经用完了所有可用的 1 MB 内存块。在 64 位中,几乎不可能创建如此多的内存映射,以至于无处可放新映射。但是,如果您最终在每个 4K 内存页面中分配了 16 字节的内存,那就浪费了很多空间。

C 和 C++ 应用程序通过使用存储池解决了这个问题。例如,对于 Web 服务器,它将启动一个包含新请求的池。在该 Web 请求结束时,池中的所有内容都会被销毁。该池构成了一个很好的、恒定大小的 RAM 块,可以反复重复使用而不会产生碎片。

垃圾收集也倾向于使用回收池,因为它避免了运行大型 GC 跟踪和连接结束时回收的压力。

在虚拟内存出现之前,一些旧操作系统(如 Apple OS 9)使用的一种方法是句柄。分配返回的是句柄,而不是内存指针。该句柄是指向内存中真实对象的指针。当操作系统需要压缩内存或将其交换到磁盘时,它会更改句柄。

我实际上已经在 C++ 中实现了一个类似的系统,它使用一个句柄数组到一个共享内存映射伪数据库中。压缩映射后,将扫描句柄表以查找受影响的条目并进行更新。

【讨论】:

  • 我听说一些 GC 将堆和堆栈视为 void* 指针的数组,并扫描它们以查找看起来像指向堆或堆栈的有效对象指针(其范围为 VM 已知)并且正确的指针对齐。他们还可以在 64 位架构上寻找毒药/标记指针的最高未使用位。不过,这样的扫描需要先停止所有线程。
  • @MaximEgorushkin 可以这样做。其他系统可以从诸如堆栈或内存分配器之类的“根”进行跟踪。它不必扫描所有内存,因为唯一可能的有效指针必须由根之一指向。另一种方法是在创建指针数组时存储它们,然后 GC 也知道每个指针的位置。
  • 好吧,有道理。
【解决方案2】:

由于成本原因,通用内存压缩通常既无用也不可取。

可能需要的是没有浪费/碎片化的内存,并且可以通过内存压缩以外的其他方法来实现。

在 C++ 中,可以为在特定应用程序中导致碎片的对象提出不同的分配方法,例如允许对象重定位的双指针或双索引;防止或最小化碎片的对象池或竞技场。这种针对特定对象类型的解决方案优于通用垃圾回收,因为它们采用了应用程序/业务特定知识,可以最大限度地减少对象存储维护的范围/成本,并且在最合适的时间发生。

A research 发现垃圾回收语言需要 5 倍以上的内存才能达到非 GC 等效程序的性能。 GC 语言中的内存碎片更为严重。

【讨论】:

  • 那篇论文的一些注意事项:它使用了 1ghz PowerPC 机器的测量结果,缓存大小很小(与现代芯片相比),并且它没有研究增量垃圾收集器。
  • @Botje 我希望看到更最新的研究。
  • 我也是。我浏览了谷歌学术中的“被引用”页面和作者的 dblp 条目,但找不到立即跟进。
猜你喜欢
  • 1970-01-01
  • 2010-11-19
  • 2014-07-18
  • 2018-01-17
  • 1970-01-01
  • 2015-10-03
  • 2010-12-13
  • 1970-01-01
  • 2017-02-06
相关资源
最近更新 更多