我正在设计一个非常简单的编译器(某种学术研究),我正在考虑实现一个简单的引用计数 GC 以及一个标记和扫描。这个想法是引用计数可以在没有循环时很早就释放死对象,这也留下了以下想法:标记和扫描通常涉及一些暂停,因为标记和扫描过程必须遍历大量引用,所以: previuos ref-counting 不应该使标记和扫描更快(更少元素)吗?这种想法是胡说八道吗?我以前从未实现过如此复杂的事情,我不想为了发现这是一个非常糟糕的主意而做很多工作。
3 回答
起初我直觉地同意,但我得出的结论是,这种直觉是错误的。虽然引用计数会在标记和清除 GC 之前删除一些垃圾(我将其简称为 msgc),但这对 msgc 性能没有多大帮助。标记阶段甚至从不查看垃圾,因此通过引用计数提前清除垃圾并不能加快标记速度。我不太确定清扫阶段,因为这取决于你如何实现它,但我可以想象有几种策略不受垃圾数量的影响,足以让这变得值得。
考虑到增加的复杂性,这可能不值得。无论如何,您不会从简单的 msgc 中获得太多性能,并且引用计数的额外成本(更大的对象标头,更慢的分配等)会减少收益,即使有一个。
delnan 是正确的,除了标记和扫描之外使用参考计数不会显着加快标记和扫描阶段,但它仍然可以起到重要的作用。
如果几乎所有对象在超出范围时都被销毁(通过引用计数),那么您的解释器将不会积累几乎一样多的内存,在这种情况下,不需要经常调用 GC。
引用计数是在恒定时间内发生的,并且性能影响很小,以至于命中实际上甚至无法与标记和扫描算法相提并论。这是一种权衡,但从用户的角度来看,每秒发生的微小停顿可能比持续很长时间的突然、任意停顿要好。当然,这取决于应用程序。
这就是 CPython 结合这两种技术的基本原理;循环垃圾收集器很少(如果有的话)需要被调用,因为如果程序没有或只有几个循环,大部分内存将被即时释放。
不过,我可以从经验告诉你,即使是一个简单的依赖于引用计数的解释器,实现和维护也是一个巨大的痛苦(特别是如果你使用的是纯 C 语言)。
如果您正在设计一个新的 GC 语言框架,则应设计确定性资源获取/处置语义,而不是事后才插入(例如,IDisposable
)。然而,引用计数不应该作为 GC 的一部分被依赖,因为在多线程环境中维护绝对健壮的引用计数是昂贵的,并且由于必须绝对避免在引用存在时内存被重用的任何可能性,即使引用计数错误地报告不再需要某个对象。请注意,过早释放资源远没有过早释放内存那么糟糕,因为释放资源的代码可能会使封装它的对象无效。即使对象已经死了,任何对它的引用都是对可识别的死对象的有效引用。相反,如果在引用仍然存在的情况下回收了内存,则引用本身将变得无效。
如果你想在一个简单的系统中最大化 GC 效率,我建议你做的是为安全地冻结存储在对象中的引用提供良好的支持[例如,提供标记的字段readonly
只能在对象被冻结之前写入构造函数,并且构造函数必须在将对象暴露给外部代码之前冻结对象]。GC 的“标记”或“复制”阶段必须检查自上次 GC 循环以来可能已写入新引用的每个对象。如果 GC 不知道哪些对象是可变的,它必须检查所有内容,或者使用写栅栏在对象在 GC 后第一次被修改时设置一个标志。然而,如果一个对象只拥有不可变的引用,那么它所引用的任何对象都必须比对象本身更早[冻结对象的生命周期将在它被“冻结”时开始——请注意,对对象的引用不会此时存在于构造函数之外]。因此,不