【问题标题】:Compacting garbage collector implementation in C++0x在 C++0x 中压缩垃圾收集器的实现
【发布时间】:2011-06-01 07:17:00
【问题描述】:

我正在 C++0x 中实现一个压缩垃圾收集器供我个人使用,我有一个问题。显然,收集器的机制取决于移动对象,我一直想知道如何根据指向它的智能指针类型来实现这一点。我一直在考虑指针类型本身中的指针指向指针,或者收集器维护一个指向每个对象的指针列表,以便可以修改它们,从而在访问时不需要双重取消引用指针,但在收集过程中增加了一些额外的开销和额外的内存开销。去这里的最佳方式是什么?

编辑:我主要关心的是快速分配和访问。我不关心特别有效的收集或其他维护,因为这并不是 GC 的真正用途。

【问题讨论】:

  • 享受头痛的乐趣,尤其是确保一切都通过所需的间接层 - 同时,我将(尽可能)用已经有垃圾的语言编写我的代码集电极。 +1 出于同情(不,这确实是一个有趣的问题)。
  • @delnan:他们必须像我一样解决这个问题,这只是在幕后。
  • “最佳”?我们需要您的要求。运行时和空间开销是否可接受? (我们应该为您猜测吗?)希望您在此阶段不要尝试对性能进行微优化。
  • 为什么不直接使用 Hans Boehm 现有的 C++ GC? hpl.hp.com/personal/Hans_Boehm/gc
  • @dajames:因为它是对new的补充,而不是替换它。

标签: c++ garbage-collection c++11


【解决方案1】:

将额外的 GC 移植到 C++ 并没有什么直接的方法,更不用说压缩算法了。目前尚不清楚您正在尝试做什么以及它将如何与其他 C++ 代码交互。

我实际上已经用 C++ 编写了一个 gc,它可以与现有的 C++ 代码一起使用,并且它在一个阶段有一个压缩器(尽管我放弃了它,因为它太慢了)。但是有许多令人讨厌的语义问题。几周前我向 Bjarne 提到过,C++ 缺少正确执行此操作所需的运算符,而且情况是它不太可能存在,因为它的实用性有限..

你真正需要的是一个“re-address-me”操作符。发生的情况是您实际上并没有移动对象。您只需使用 mmap 更改对象地址。这要快得多,实际上,它使用 VM 功能来提供句柄。

如果没有此功能,您必须有一种方法来执行对象的重叠 移动,而这在 C++ 中无法有效执行:您必须首先移动到临时对象。在 C 语言中,这要容易得多,您可以使用 memmove。在某个阶段,必须调整所有指向或进入移动对象的指针。

使用句柄不能解决这个问题,它只是将问题从任意大小的对象减少到恒定大小的对象:这些在数组中更容易管理,但同样的问题存在:你必须管理存储。如果您从数组中随机删除大量句柄..您仍然会遇到碎片问题。

所以不要打扰手柄,它们不起作用。

这就是我在 Felix 中所做的:你打电话给 new(shape, collector) T(args)。这里的shape 是一个类型的描述符,包括一个包含(GC)指针的偏移列表,以及用于完成对象的例程的地址(默认情况下,它调用析构函数)。

它还包含一个标志,说明是否可以使用memmove 移动对象。如果对象很大或无法移动,则由malloc 分配。如果对象很小且可移动,则将其分配在一个竞技场中,前提是该竞技场中有空间。

竞技场是通过移动其中的所有对象来压缩的,并使用形状信息来全局调整指向或进入这些对象的所有指针。压缩可以增量完成。

C++ 程序员的缺点是需要构造一个正确的shape 对象来传递。这并不困扰我,因为我正在实现一种可以自动生成形状信息的语言。

现在:关键点是:要进行压缩,您必须使用精确的收集器。压缩不能与保守的收集器一起使用。这个非常重要。如果你看到一个看起来像指针但恰好是整数的值,允许一些泄漏是可以的:一些对象不会被收集,但这通常没什么大不了的。但是对于压缩,你必须调整指针,但你最好不要改变那个整数:所以你必须知道确定什么时候是指针,所以你的收集器有准确地说:形状必须是已知的。

在 Ocaml 中,这相对简单:一切都是指针或整数,低位在运行时用于指示。指向的对象有一个代码告诉类型,并且只有几种类型:标量(不要扫描它)或聚合(扫描它,它只包含整数或指针)。

【讨论】:

  • 有了句柄,我可以看到为什么句柄池的大小将是曾经存在的最大句柄数,但是如果句柄不可共享(代码想要一个引用的副本存储在句柄必须构造一个新句柄,将引用从旧句柄复制到新句柄,然后在不再需要时破坏其新句柄)我认为它们可以快速轻松地回收。除非一项任务需要大量句柄,然后再也不需要这么多的句柄,否则我不明白为什么碎片会成为问题。
【解决方案2】:

这是一个非常直截了当的问题,所以这里是一个直截了当的答案:

Mark-and-sweep(偶尔mark-and-compact 以避免堆碎片)在分配和访问方面是最快的(避免双重取消引用)。它也很容易实现。由于您不担心收集性能影响(标记和清除往往会在不确定的情况下冻结该过程),这应该是要走的路。

实现细节见:

【讨论】:

  • 这似乎很容易——如果堆栈、本机堆或静态分配上存在唯一的 GC 引用。我将如何处理来自本机堆的 GC 引用?
  • 我的意思是堆栈、GC 堆或静态分配。
  • -1 实际上mark-sweep的分配性能比较差。分代提供快速分配,通常与最老一代的标记扫描结合使用。
【解决方案3】:

nursery generation 会给你最好的分配性能,因为它只是一个指针碰撞。

您可以使用影子堆栈等技术在不使用双重间接的情况下实现指针更新,但如果您手动编写此 C++ 代码,这会很慢并且很容易出错。

【讨论】:

  • 实际上,如果你在包装 C++ 代码,最好的性能是通过避免 GC 获得。一般来说,全局 GC 是非常糟糕的。它不能扩展,它会清除你的缓存,不会玩多处理等等。[看看 Ocaml,仍然停留在单线程领域] GC 在你可以分区和本地化操作之前不会变得可行,并在不需要时避免 GC。 FPL 中的拳击在这里是邪恶的。 Rust 语言旨在实现其中许多目标,但它放弃了多线程(共享内存并发)来实现这一目标。
  • 很清楚:Felix 使用了一个非常缓慢的幼稚收集器,它不敢移动东西(压实机掉了)。然而,这并不意味着它很慢,因为它也可以像传统的 C++ 列表对象那样做不需要任何 GC 的事情:它使用抽象将对象节点与外界隔离。 GC 需要扫描列表值中的指针,但从不需要获取节点。因此,使用 C++,我们在程序的一部分中消除了对 GC 的需求。确实只有在有循环时才需要 GC。
猜你喜欢
  • 2015-07-13
  • 2011-10-15
  • 1970-01-01
  • 2013-04-01
  • 2016-05-19
  • 2013-03-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多