2

llvm 文档说:

然而,在实践中,使用积极的垃圾收集技术的局部性和性能优势主导了任何低级损失。

那么,究竟是什么导致了使用垃圾收集而不是手动管理内存时的性能提升?(除了代码编写时间的明显减少)执行堆压缩的好处仅仅是增加了空间局部性和缓存利用率吗?还是有其他帮助更大的方法,例如一次删除所有内容?

4

4 回答 4

2

我只能代表 Oracle(前 Sun)和 IBM JVM;它们的效率取决于新创建的对象不太可能存在很长时间的事实。因此,将它们隔离到自己的区域可以使该区域经常被压实,因为幸存者很少,这是一项廉价的操作。频繁的压缩意味着空闲空间可以保持连续,因此对象创建也很便宜,因为没有空闲链可以遍历,也没有内存碎片。

手动内存管理方案很少有这种效率,因为这是一种相对复杂的做事方式,不太可能为每个应用程序重新发明。这些垃圾收集器经过更长时间的发展和优化,并且比单个应用程序所付出的努力更多。如果它们的性能不高,那将是令人惊讶和失望的。

于 2012-05-17T22:22:19.847 回答
2

在现代处理器上,内存缓存为王。遭受高速缓存未命中可能会使处理器停顿数百个 cpu 周期,等待慢速总线提供数据。

使缓存有效需要参考的局部性。换句话说,如果下一次内存访问接近前一次,那么数据已经在缓存中的可能性很高。

垃圾收集器可以帮助很好地解决这个问题。最大的胜利不是集合,而是它在这样做的同时重建对象图和重组数据结构的能力。压实。

想象一下典型的数据结构,一个指向对象的指针数组。例如,从文件中读取一堆字符串并将它们转换为对象的字段值时,它正在慢慢建立起来。分配的对象将分散在地址空间中这样做。由工作对象分隔的数组指向的长寿对象,如字符串。稍后迭代该数组将非常缓慢。

直到垃圾收集器运行并重建数据结构。将所有指向的对象按顺序排列。

现在迭代集合非常快,因为访问元素 N 使得元素 N+1 很可能随时可用。如果不在 L1 缓存中,那么 L2 或 L3 的可能性非常大(如果有的话)。

非常大的胜利,这是使垃圾收集与显式内存管理竞争的一项功能。显式类型存在不支持移动对象的问题,因为它会使指针无效。

于 2012-05-17T23:11:54.657 回答
1

我怀疑局部性是否有助于性能 - 诚然,小对象倾向于在堆的同一区域同时创建(但这也适用于 C),随着时间的推移,这些剩余的小对象将被压缩成一个紧密的堆的相关区域,据说这给了你优于 C 风格分配的优势。但是,给我看一个只使用这些小对象的程序,我将给你看一个可以做所有事情的程序。给我看一个程序,它传递要在堆栈上使用的所有对象,我会告诉你一个快速尖叫的程序。

内存的解除分配是短期的性能优势,因为它们不需要被解除分配。然而,当垃圾收集器启动时,这种好处就消失了。但是,通常情况下,当系统(理论上)没有发生任何其他事情时,就会发生收集,因此成本实际上是无效的。

堆的压缩也有助于分配,所有分配都可以来自堆的开头,内存管理器不必遍历堆寻找下一个合适大小的空闲空间块。但是,传统系统可以通过使用多个固定块堆来获得相同的速度(这意味着您始终从堆中分配所需大小的块,并且始终分配固定块,因此遍历堆只是为了找到第一个空闲块,这可以使用位图删除)

所以总而言之,除了基准测试之外,根本没有什么好处。根据我的经验,GC 可以而且会在错误的时间介入并显着减慢您的速度,通常是当系统内存被填满时,因为用户已经完成了诸如加载需要大量内存分配的新页面之类的操作...... . 这反过来又需要一个集合。

它也有使用大量内存的趋势——“内存便宜”是 GC 语言的口头禅,因此在编写程序时考虑到这一点,这意味着内存分配更为常见,尤其是对于临时对象和中间对象。只需查看 StringBuilder 类即可找到众所周知的证据。使用它可以“解决”字符串,但许多其他对象仍然被随意分配。任何使用大量内存的程序都会发现自己在 RAM IO 上苦苦挣扎——所有内存都必须放入 CPU 缓存中才能使用,你使用的内存越多,你的 CPU MM 必须做的 IO 就越多,这可以在错误的情况下杀死性能。

此外,当发生 GC 时,您也必须处理 Finalized 对象,这并不像以前那么糟糕,但它仍然可以在运行 finalisers 时停止您的程序。

旧的 Java GC 的性能很差,尽管很多研究已经使它们变得更好,但它们仍然不是完美的。

编辑:关于本地化的另一件事,想象创建一个数组并添加一些项目,然后进行分配负载,然后您想向数组中添加另一个项目 - 使用 GC 系统,添加的数组元素将不会被本地化,甚至压缩后,数组中的每个对象都将作为单独的项目存储在堆上。这就是为什么我认为本地化问题并不像人们想象的那么重要。现在,将其与分配有缓冲区的数组进行比较,并且在缓冲区空间内分配对象。这可能需要重新分配和复制才能添加新项目,但读取和修改它非常快。

于 2012-05-17T22:39:27.690 回答
0

一个尚未提及的因素是,特别是在多线程系统中,有时很难确定地预测哪个对象最终会持有对其他对象的最后一个幸存引用。如果不必担心可能包含循环的对象图,则可以为此目的使用引用计数。在复制对对象的引用之前,增加其引用计数。在销毁对对象的引用之前,请减少其引用计数。它减少引用计数使其达到零,破坏对象以及引用。这种方法适用于只有一个 CPU 内核的计算机;如果在任何给定时间实际上只能运行一个线程,则不必担心如果两个线程尝试调整同一对象会发生什么” s 引用计数同时进行。不幸的是,在具有多个 CPU 内核的系统中,任何想要调整引用计数的 CPU 都必须与所有其他 CPU 协调该操作,以确保两个 CPU 不会同时击中计数器。这种协调对于单个 CPU 来说是“免费的”,但在多核系统中相对昂贵。

当使用批处理模式垃圾收集器时,对象引用通常可以自由分配、复制和销毁,而无需 CPU 间协调。需要定期让所有 CPU 停止并运行垃圾回收周期,但要求所有 CPU 每隔几秒左右相互协调一次比要求它们在每个 CPU 上相互协调便宜得多对象引用赋值。

于 2012-05-21T21:54:03.217 回答