使用参考计数混合标记和扫描

时间:2013-05-07 16:20:37

标签: compiler-construction garbage-collection runtime

我正在设计一个非常简单的编译器(一种学术研究),我正在考虑实现一个简单的引用计数GC以及Mark和sweep。这个想法是,引用计数可以在没有周期时很快释放死对象,这也留下了以下想法:标记和扫描通常涉及一些暂停,因为标记和扫描过程必须遍历大量引用,因此:难道不能通过先前的重新计数来使标记和扫描速度更快(更少的元素)吗? 这个想法是胡说八道吗?我以前从来没有实现过这么复杂的事情,我不想做太多工作只是为了发现这是一个非常糟糕的主意。

3 个答案:

答案 0 :(得分:3)

我最初直觉地同意了,但我得出的结论是这种直觉是错误的。虽然ref-counting将在标记和扫描GC之前删除一些垃圾(我将其简称为msgc),但这对msgc的性能没有多大帮助。标记阶段甚至看不到垃圾,因此通过引用计数尽早清除垃圾并不会加快标记速度。我不太确定扫描阶段,因为这取决于你如何实现它,但我可以想象一些不受垃圾量影响的策略,足以让它变得有价值。

考虑到增加的复杂性,它可能不值得。无论如何,你不会从一个简单的msgc中获得太多的性能,并且如果有一个,则refcounts(更大的对象标题,更慢的赋值等)的额外成本会减少增益。

答案 1 :(得分:2)

delnan是正确的,除了mark& amp;扫描不会加速标记&扫描阶段显着,但它仍然可以起到重要作用。

如果几乎​​所有对象一旦超出范围就被销毁(通过引用计数),那么你的解释器将不会积累相当多的内存,在这种情况下GC不需要经常调用

参考计数在不变的时间内发生,并且性能受到如此小的影响,以至于命中率实际上甚至不能与标记&扫描算法。这是一种权衡,但从用户的角度来看,每秒发生的微观停顿可能比持续显着时间的突然,任意停顿更好。当然这取决于应用程序。

这就是CPython结合这两种技术的基本原理;循环垃圾收集器很少(如果有的话)需要被调用,因为如果程序没有或只有几个周期,大多数内存将被动态释放。

我可以从经验告诉你,实现和维护一个依赖引用计数的简单解释器可能是一个巨大的痛苦(特别是如果你使用普通的C)。

答案 2 :(得分:1)

如果您正在设计一个新的GC语言框架,那么确定性资源获取/处置语义应该被设计为而不是作为事后想法插入(例如IDisposable的情况)。但是,不应该依赖引用计数作为GC的一部分,因为在多线程环境中保持绝对健壮的引用计数是昂贵的,并且因为必须绝对积极地避免在引用存在时重用内存的任何可能性,即使引用计数错误地报告不再需要对象。请注意,过早释放资源远不及过早释放内存那么糟糕,因为释放资源的代码可能使封装它的对象无效。即使对象已经死亡,对它的任何引用都将是对可识别死对象的有效引用。相反,如果在引用仍然存在的情况下回收内存,则引用本身将变为无效。

如果你想在一个简单的系统中最大限度地提高GC的效率,我建议你做的是为安全地冻结存储在对象中的引用提供良好的支持[例如规定标记为readonly的字段只能在对象被冻结之前写入构造函数中,并且构造函数必须先冻结对象,然后再将其暴露给外部代码。 GC的“标记”或“复制”阶段必须检查自上一个GC循环以来可能已将新引用写入其中的每个对象。如果GC不知道哪些对象是可变的,那么它必须检查所有内容,或者在GC存活后第一次修改对象时使用写入围栏来设置标志。但是,如果一个对象只包含不可变引用,那么它所持有引用的任何对象必须比对象本身更旧[冻结对象的生命周期将在它被“冻结”时开始 - 请注意,不会引用该对象在那一点上存在于构造函数之外]。因此,在执行更高代的集合时,垃圾收集器可以完全忽略不包含任何可变引用的对象。