两个星期后读到Billy Newport的“多内核可能不适合Java”这篇日志时,我感到非常吃惊。我在JRockit团队中做过大量Java测试工作,没有看到任何多内核芯片会导致性能问题的迹象。Billy Newport给出的一则评论是“垃圾收集仍然非常依赖于短时间暂停的时钟速度”。我认为这表示他相信GC无法在多内核芯片上很好地进行缩放。此外,这违背了我们的经验,所以我决定对此进行测试。
测试设置
GC暂停时间通常是堆大小和活动数据(如非垃圾数据)的函数,但它会有所变化,这取决于具体的堆布局。只查看单个暂停不会产生让人非常信服的数据,因此我决定寻找一个具有固定数量的活动数据和固定堆大小的基准,通过长时间的运行查看大量GC暂停的暂停时间。我决定使用一个已经定义好的工作负载,即SPECjbb2005基准。通常,此基准通过增加仓库在运行期间线性地增加负载,但我将它配置为使用固定仓库集(8)。每个仓库都包含一定数量的活动数据(大约35MB)和一些基本数据。然后我在逐渐增加JRockit使用的GC线程数量的同时多次运行该基准。每次进行这样的运行时,我都记载了暂停时间的中间值,以减少对不固定的长时间或短时间GC暂停的影响。
测试设置包括单内核、双内核和四个内核的Intel系统,以及6个内核的Sun T1服务器。以下是详细信息:
Intel Xeon 2.2 GHz,具有超线程(4CPU线程)处理技术的2个CPU。这是一个相当老的单内核系统。
Intel Xeon "Woodcrest" 2.67 GHz,2个CPU,4个内核
Intel Xeon "Clovertown" 2.33 GHz,2个CPU,8个内核
Sun T2000 1000 MHz,1个CPU,6个内核,24个线程
JVM命令行是:
-Xms2g -Xmx2g -Xgc:parallel -XXgcthreads=N
即一个固定的2GB的堆和一个让所有东西停止的单空间并行压缩的标记和清除GC,以及变化的GC线程数量。
在继续探寻结果之前,我要指出的是,GC的频率也会随CPU性能而有所不同。比较快的芯片会产生更多的SPECjbb2005 bops,并在给定时间帧里收集更多的内存,堆填充的速度更快,因此GC会更频繁。但是,这不会影响GC暂停时间,所以与这次实验无关。
期望的结果
要记住,GC实质上意味着遍历内存和移动对象。这意味着性能瓶颈会是CPU或内存子系统。结果看起来应该如下所示:


两幅图中的第一幅图将GC暂停时间显示为GC线程数量的函数,而第二幅图则描述了GC可伸缩性。如果CPU是瓶颈,我们期望第二个GC线程将暂停时间削减为一半,而第三个GC线程将暂停时间减为原来的三分之一,依此类推。可缩放性图应该线性地缩放内核数量。如果看见此行为,就可以假定GC实现的缩放很成功。
如果内存是瓶颈,我们仍然可以很好地缩放至内存总线饱和的那个点上,这之后,曲线会呈现为一条直线。在这种情况下,如果不进行进一步的分析,则无法确定是JVM中的GC实现出错了,还是到达了硬盘限制。
当然,我们无法期望看到一个完美的结果。内存访问速度不是恒定不变的,而是会受到多级CPU缓存的影响,GC线程可能会因为等待内存而延迟(如果没有适当的CPU缓存)。我们可能还要面临序列化问题,因为事实上JRockit 必须确保GC线程对Java堆的安全并发访问。CPU线程可能会以多种方式影响性能,尽管人们希望获得更好的性能。
Intel Xeon
以下是从三种Intel机器获得的结果:


在完美的情况下,会在Clovvertown和Woodcrest上看到8x和4x,但实际的数量大约分别是5x和3x。我的猜测是,通过进一步的JVM优化,可以获得更接近完美的可缩放性,但显然对于Intel x86芯片上的更多内核,JRockit的GC实现的缩放已经很合理。
Sun T1
Sun公司说他们的硬件是为通过量而设计的,这意味着我们应该期望获得非常好的可缩放性,但单线程性能可能有些差强人意。例如,如果JRockit中的GC实现的缩放太差,那么GC暂停时间会非常长。
此外,当服务器是6核T2000时,我们一定不会忘记它有一个每个内核处理四个线程的CPU线程实现。线程实现是基于事件切换的多线程(Switch on Event Multi-threading,SoEMT),但Sun使用了营销名称“Coolthreads”。我在这里不准备解释其工作原理(留待他人解释),但重要的是我们可以期望可缩放性超出物理内核数量的限制。


可缩放性取决于内核的数量(6),它们的关系是近线性的(5.7x),我们从CPU线程处理实现中可获得额外的改进。优化结果是使用21个线程获得的——大约是物理内核的三倍。在基准提交时,这似乎也是Sun的选择,因此,对于硬件而言,这可能是一个好选择。关于此结果的一个次要问题是,当增加的线程数量超过最佳数量时,性能实际上会下降,这种现象在其他硬件上看不到。
单线程与多线程GC性能
查看极端情况,以下是在各种架构上使用单个GC线程的GC暂停时间:

使用最佳数量的GC线程的暂停时间(在Xeon上,该数量等于进行HT操作时的内核或CPU线程的数量,在T1上,该数量大约等于内核的三倍):

单线程性能在Woodcrest和Clovertown CPU上表现非常好——大约比旧的Xeon芯片快两倍。在T1上,单线程性能非常差,但因为GC缩放性很好,所以这在实践中不是问题。
总之,我认为这些结果证明JRockit的GC实现在多内核系统上的缩放性非常好,我相信GC实现可缩放性能够得到进一步提高,即使将来缩放更多数量的内核也应该没有问题。对于Newport先生的博客中提到的其他问题,存在以下观点:
1)与其他许多语言相比较,Java中的线程处理相对简单些,并且许多Java应用程序已经是多线程的。
2)Java可以使用运行时分析来优化锁定、内存分配和其他低级操作,以便使用静态兼容语言(如C语言)无法轻松做到的方式实现多线程使用,这有助于多内核硬件上的可缩放性。
3)即使用户代码无法缩放,JVM也可以使用空闲的CPU循环来实现后台入库,比如并发GC或用更多侵略性优化重新编译代码。
结束语
朝向更多内核发展的趋势显然会迫使Java编程人员适应。不过,JRockit从利用当前多内核芯片处理关键任务中获益非浅,这些任务包括GC,还包括减少对多内核系统上正运行操作的可缩放性影响的其他优化。我没有理由去相信其他JVM更糟糕。由于JVM提供了一个好的基础,以及该语言为多线程提供的易用性,所以我的结论是,多内核非常适合Java!
原文出处:http://dev2dev.bea.com/blog/hstahl/archive/2006/12/multicore_is_go.html