简单说,单纯的引用计数做不了GC。这是因为引用计数存在循环引用的问题:比如A引用了B,B又引用了A,当其它对象全部删除了自己对A和B的引用时,A和B会因为彼此引用而永远不能被回收,这就会造成内存泄漏。
如果你想弥补这个缺陷,那么就不得不另外建立一张“网”,通过这个网找到A和B,检测它们是否存在循环引用——甚至于,是否存在A->B->C->D->E->F->A这样的大循环、甚至是否有错综复杂的依赖网问题。
那么,这套补足算法必然是非常低效的:第一,它必须定期执行(如何确定执行时机?);第二,它必须遍历所有现存对象;第三,它必须执行一套复杂的循环引用发现算法。
换句话说:引用计数不做GC、而是作为某种轻量级的GC代替品时,它是高效的、行为极度可控的;但一旦想基于它做GC,这东西就完蛋了。
而在C++里面,从没有人拿引用计数当GC用。
事实上,当我们费劲巴拉的去“发现循环依赖”时,我们其实就实现了一个mark and sweep或者分代GC方案——于是乎,我们就相当于在一个常规GC之外、额外维护了一套引用计数体系,以实现“资源的即时释放”。
换句话说,并不是“引用计数性能比GC差”,而是“在GC之外额外维护一套引用计数体系就会额外付出很多性能”——再换句话说,为了“资源的即时释放”而在常规GC之外附加一套引用计数系统是得不偿失的。
这就是“引用计数当GC用性能差“的根本原因。
但如果不把引用计数当作GC来用、而是审慎有节制的用于受限场景时,引用计数就是既轻量又及时的完美品——完美到极大的压制了“给C++引入真正的GC”的需求。
当然,你的确可以构造一些特殊场景,让“引用计数”相对于GC算法显得低效。
比如,给一组对象反复addref/release,那么引用计数在每次add/release时都不得不更新ref_count(尤其多线程环境下,每次更新都还伴随着性能消耗严重的加锁操作);而GC呢,人家可以视若无睹,直到定时时间到,于是stop world并扫描一次——引用计数被指使着做了无数的无用功,而GC仅仅做了一次批量操作,那么性能表现当然会远高于前者。
但这种场景是极其少见的。尤其C++程序员早就习惯了通盘考虑资源的分配/归还操作、加上语言本身区分的比较严格,因此除非刻意攻击,否则引用计数的效率——无论空间还是时间效率——以及各方面的可靠性可控性都是无与伦比的。
不过,在其它语言中,由于GC的存在以及滥用,加上语言本身的限制,无效的引用就会特别多;那么对于无视引用添加/撤销、只会在时间到后算总账的GC,这并不会带来任何负面影响;可一旦用了引用计数,这种语言/语法习惯就会制造出海量的多余引用,就会大量消耗性能。
换句话说,在没有GC、习惯了手动资源分配回收的C++里面,用起来几乎不需要付出什么代价的引用计数是如此优越,以至于都抑制了给它添加GC的需求——引用计数是如此的好用、轻量,我们干嘛要引入一个难以完全控制的GC呢?
但在基于GC构筑的语言里,前面提到过,引用计数本就不能实现GC;哪怕勉强拿它实现了GC,实质上也相当于“GC之外额外加了一套引用计数”——那么对这些已经有了GC的语言,这就相当于把GC的工作通过引用计数分散到程序执行的每时每刻、使得GC执行时可以少检查一些对象:换句话说,付出了很多时间和空间代价,换来了GC执行时工作量的减少。
这在某些情况下的确有奇效。或者说,也可以精心构造一个GC+引用计数相对于GC有利的场景:比如不停的分配和归还新对象,引用计数可以确保这些对象可以第一时间被归还,不会过多占用内存——如果没有引用计数,那么GC执行前就会有海量内存得不到释放、且GC执行时需要遍历海量对象,需要“卡顿”更长时间才能完成释放。
换句话说,GC+引用计数方案有效降低了程序平时的内存空间占用量、降低了GC执行时world stop的必需时长。
但同样的,这种情况也不算多见(比起大量添加删除引用还是更常见一些),在正常场景下是得不偿失的。
最后,总结一下。
引用计数并不是GC;“引用计数实现GC”本质上是给一套常规GC附加了一套引用计数系统,多耗了执行时间,换来的好处却寥寥无几。
引用计数的优点:
引用计数的缺点:
GC的优点:
GC的缺点:
GC+引用计数的优点:
GC+引用计数的缺点:
正是因为引用计数的种种优点,C++才选择了它(而不是GC);这是C系语言一贯的“相信程序员”思想的延续:语言设计者相信他们能用好这个难用的东西、写出效率最高的程序。
而其他程序选择了“尽量降低门槛”,因此才选择了GC,替程序员下了“空间换便利”的决定——对C++程序员来说,这是不可接受的。因为他们常常需要把机器性能挖到极致。
至于GC+引用计数(或者说“引用计数实现GC”)嘛……相对于它带来的一点点好处,付出的代价实在太重,因此几乎没有什么人会选择这个方案。
哪里有说过“引用计数在做 GC 时有性能问题”?
具体是什么性能问题?