垃圾收集器如何比显式内存释放更快?

11

我在阅读这篇生成的HTML(可能会过期),内容来自原始PS文件

GC迷思3:垃圾回收器总是比显式内存释放慢。
GC迷思4:垃圾回收器总是比显式内存释放快。

这让我很困惑。GC怎么可能比显式内存释放更快呢?它不就是在释放内存或使其再次可用时调用显式内存分配器吗?所以...它到底是什么意思呢?

非常小的对象和大型稀疏堆==>GC通常更便宜,特别是对于线程

我还是不明白。这就像说C++比机器码更快一样(如果你不理解这个句子中的 WTF,请停止编程。让 -1 开始吧)。经过快速的谷歌搜索,有一种说法是当你拥有大量内存时,GC会更快。我的理解是,它不会受到释放的影响。当然,这可能很快,我也写过一个自定义分配器,在一个应用程序中根本不释放任何对象(只有在终止时才释放),而且在库和STL之类的情况下定义得比较多。因此...我相信这也会比GC更快。如果我仍然希望释放,我想我可以使用一个使用deque或其自己的实现的分配器,这些分配器本质上是

if (freeptr < someaddr) {
    *freeptr=ptr;
    ++freeptr; 
}
else
{
    freestuff();
    freeptr = freeptrroot;
}

我相信这种方式肯定非常快。我已经回答了自己的问题。如果GC收集器从未被调用,那么它会更快,但是...我确信文档所指的两个收集器都会被测试到,因此这不是文档的意思。无论使用哪种GC,如果GC收集器被调用一次,我相信同一个应用程序的速度都会变慢。如果已知永远不需要释放,则可以使用空的释放体,就像我遇到的某个应用程序一样。

总之,我发布这个问题是为了进一步了解。


7
示例失败。实际上,C++ 可以比机器码更快,因为 C++ 编译器在生成接近完美的机器码方面比汇编程序员出色,尤其是当人类已经放弃理智时,编译器仍可以非常一致地生成数十亿条指令。这还假设你所说的是给定程序的运行时性能,而不包括程序是否能完成。 - user395760
@delnan:我不是指“编写”。对于 SSE 等情况,编译器不知道可以使用特殊指令或者在哪里使用。 - user34537
1
你低估了编译器。很多编译器早就开始使用它们了。还可以参考https://dev59.com/EXRC5IYBdhLWcg3wG9Xo,虽然不完全符合这个问题,但其中包含了一些GCC生成几乎最优代码的例子。 - user395760
VC++生成了优美的SSE代码。只需在“属性”中设置选项,就可以看到它的效果。 - Jive Dadson
3
实际上,对于更大规模的所有内容,C++可以比机器码更快,因为C ++编译器在编写接近理想机器码方面比汇编程序员更出色。多年来,我研究了这个问题很多次,一次又一次地发现并不是真的:编译器生成的机器码相当糟糕。去年我再次研究了它,并再次发现,即使没有x86专业知识,我也能够比GCC编写更好的汇编代码。http://flyingfrogblog.blogspot.co.uk/2012/04/x86-code-generation-quality.html - J D
4个回答

28
GC如何比显式内存回收更快呢?
1. GC可以将指针顺序分配到本地线程的代中,然后依靠复制收集来处理疏散幸存者的(相对)不常见的情况。传统的分配器(例如malloc)经常竞争全局锁和搜索树。 2. GC可以通过重置线程本地分配缓冲区而不是逐个调用free释放每个块来同时释放许多死块,即O(1)而不是O(n)。 3. 通过压缩旧块,使更多的块适合每个高速缓存行。改进的局部性增加了高速缓存效率。 4. 利用额外的静态信息,例如不可变类型。 5. 利用额外的动态信息,例如通过写屏障记录的堆拓扑的变化。 6. 通过从无等待算法中删除手动内存管理的麻烦,使更有效的技术易于执行。 7. 将释放延迟到更适当的时候或将其卸载到另一个核心。(感谢Andrew Hill提出这个想法!)

1
通过将内存释放延迟到最小化 CPU 竞争的最佳时间(以少量额外的 RAM 使用为代价),并在不同的线程中执行,从而避免主线程执行。 - Andrew Hill
这几乎可以理解为:一个“非常精心设计和优化”的最先进的GC将击败一个“天真”的传统分配器。我认为这不是一个公平的比较。这7个中哪些对于更高级的分配器来说真的是不可能的呢?例如,分配器也可以跟踪线程本地存储、搜索树并尝试防止全局锁定。 - André
从分配器本身来看,1到6都是不可能完成的。 - J D

14

使GC比显式回收更快的一种方法是隐式回收:

堆被划分为多个分区,虚拟机会定期切换这些分区(例如当某个分区变得太满)。活动对象被复制到新的分区中,所有死亡对象不会被释放 - 它们只是被遗忘。因此,回收本身实际上不需要花费时间。这种方法的另一个优点是堆的碎片整理成为免费奖励。

请注意,这只是对实际过程的非常一般的描述。


我喜欢它。我从未想到过由于碎片整理和将内存适配到较少的缓存页面而获得速度奖励。很好。+1 - user34537

10

关键在于,垃圾回收器的底层分配器可以比显式分配器简单得多,并采取一些显式分配器无法使用的捷径。

  1. 如果收集器是复制式(像Java、.NET、OCaml和Haskell运行时等),释放是以大块为单位进行的,而分配只是指针增量,成本则是每个对象在集合后幸存下来的代价。因此,它更快,尤其是当存在许多短暂的临时对象时,这在这些语言中相当常见。
  2. 即使对于非复制式的收集器(如Boehm的收集器),对象释放也是分批进行的,这可以节省大量在组合相邻空闲块时所需的工作。因此,如果不需要经常运行垃圾收集,就很容易实现更快的效果。
  3. 此外,许多标准库malloc/free实现是低效的。这就是为什么有项目像umem和类似glib库的轻量级版本的原因。

1
一个问题。指针的增量是如何工作的?我在想像ptr+=chunksize这样的东西不会起作用,因为内存中会有很多空洞,你不知道哪个是可用的。所以也许可以有一个列表来记录哪些内存已经移动,并且每次找到它时更改指针。后者听起来可能有效,但这只是猜测。你知道一个好的实现方法吗? - user34537
4
@acidzombie说:复制式垃圾收集器(Copying GC)会预留一大块内存,并使用该内存来进行操作。指针的移动仅限向上。在回收时,指针不会减小,这是为了避免处理空洞(除非当“顶部”被释放时,即指针和被释放对象之间没有存活对象时,可以考虑作为一种优化)。当当前区域用尽时,所有存活对象将被复制到一个新的、无空洞的区域中,并且指针将位于最后一个对象之后。(也可参见 http://blogs.msdn.com/b/abhinaba/archive/2009/02/02/back-to-basics-copying-garbage-collection.aspx) - user395760

1
一个尚未提到的因素是,当使用手动内存分配时,即使保证对象引用不会形成循环,确定最后一个持有引用的实体何时放弃它可能是昂贵的,通常需要使用引用计数器、引用列表或其他跟踪对象使用情况的方法。这些技术在单处理器系统上并不太糟糕,其中原子增量的成本可能与普通增量基本相同,但在多处理器系统上,原子增量操作相对昂贵,因此不易扩展。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接