构建健壮的Java基准测试

Posted in java on 八月 15th, 2010 by kafka0102

本周遇到几篇和基准测试相关的不错的文章,如果不是因为上周末鼓弄了一下各种锁的性能测试,我或许就会错过它们。两篇文章来自dw,分别是 Robust Java benchmarking, Part 1: Issues 和  Robust Java benchmarking, Part 2: Statistics and solutions,作者还有个专页 Java benchmarking article 提供一个Java基准测试的框架,感兴趣的可参考之。本文算是对Robust Java benchmarking, Part 1: Issues的一个简单的总结。在阅读Robust Java benchmarking两篇文章的过程中,我也看了些其中的参考文章,也有一些不错的可以拜读之。

1、度量执行时间

基准测试通常的过程是:1)记录开始时间,2)执行代码,3)记录结束时间,4)计算时间差。Java中记录时间程序员们经常使用System.currentTimeMillis,不过该方法在精度上有偏差,这个偏差依操作系统的不同而不同,比如Win98可能会偏差55ms,Linux2.6可能偏差1ms。如果测试的task执行时间在毫秒级别,这个偏差就会影响结果的正确性了。JDK1.5中引入的System.nanoTime就是更好的选择,它在精度和准确性方面表现得都更好。而这种偏差以及其他因素可能引起的偏差,都在提醒我们,做基准测试时要使得task执行较长的时间。

2、代码预热

Java的执行过程是很复杂的,这其中会有很多因素影响到基准测试。通常来说,Java代码在开始执行阶段会相对很慢,之后会越来越快,直到达到稳定阶段,这一过程涉及的影响因素主要有:
1)类加载。类加载涉及到文件读取、解析、校验等系列操作,所以在计算真正的task执行前需要先执行几遍task确保类加载都完成了。如果task涉及的条件分支很多,要确保各分支的代码都覆盖到。
2)及时编译。JVM执行的是Java代码被翻译成的字节码,字节码的解释执行速度多数情况下是要比执行机器码慢的。所以,在Java代码执行过程中,JVM会根据执行情况将“热”的字节码编译成机器码加速执行,这就是JIT。Sun(Oracle?)的HotSpot JVM有两种启动模式,即client和server。启动时,client相比server做的优化更少,所以启动得更快。在运行时,同样一段代码,client需要1500次调用server需要10000次调用,使得JVM会将这段“热”代码编译成机器码。所以,如果要使得基准测试是在稳定阶段进行,就需要JVM将task中的代码动态编译成机器码。可以使用CompilationMXBean.getTotalCompilationTime函数以及-XX:+PrintCompilation启动参数查看及时编译情况。
为解决上述两点提到的问题,一个可行的执行基准测试的过程如下:
1. 执行task一次去加载所有的类。
2. 执行task足够多次确保JVM的执行达到稳定阶段。
3. 执行task一些次以得到task执行时间的评估值。
4. 使用步骤3计算n,n是接下来task的执行次数,它要使得task的累积执行时间是足够的大。
5. 度量执行n次task的总的执行时间。
6. 评估单次(t/n)task的执行时间。

3、动态优化

动态编译不是一劳永逸的,它还涉及到一系列问题,突出的问题如下:
1)Deoptimization:在Java程序运行过程中,同一段代码并不一定只被编译一次,JVM可能根据执行情况对已经编译过的代码段重新编译,以取得更好的优化效果。因为JVM通常编译的是一段“热”代码,但在运行时可能“热”代码周边的代码(比如一些并不经常走的分支)也“热”起来,这时整个代码段就可能需要重新编译。
2)On-stack replacement:早期的HotSpot在编译“热”方法时,是在下次调用该方法时使用编译的字节码,本次的方法执行还是解释执行。所以,如果该次方法调用后再不调用这一方法,那么这个编译就没什么价值。比如一个极端的例子,在main函数里执行长循环操作,尽管HotSpot及时编译了机器码,但在循环执行过程中用不上,当循环结束,main也结束了,结果白白编译了机器码。所以,HotSpot后来引入了OSR,可以在方法执行过程中字节码替换成机器码。尽管OSR看起来很好,但OSR往往没有对编译的代码做最优化处理,这对基准测试来说就不是最好的选择。为避免OSR的问题,通常不要把所有的代码放到一个方法里。
3)Dead-code elimination:Dead-code的情况很多,比如调用一个有返回值的方法,但返回值从来没有被调用者接收处理,JVM就可以对此做优化。这种优化在很多语言里都有,但对基准测试来说,这可不是好的情况。所以,写基准测试代码时要注意这个问题。

4、Resource reclamation

garbage collection and object finalization (GC/OF)是JVM自己的行为,如果程序员任其发展,它往往会在你不知道的时刻影响到基准测试。减少GC/OF的影响,通常有两种方法:1)是task运行时间长些,平衡掉GC的影响,2)是执行足够遍的task,并在每次执行后做System.gc和System.runFinalization处理,化被动为主动。

5、Caching

OS Cache和CPU Cache有时也会影响到基准测试。如果是测试文件IO操作,就不能忽略OS Cache的影响。如果是针对数值做测试,就可能要考虑CPU Cache的影响。

6、其他因素

除了上述几点,一些外界因素也需要考虑,比如测试执行过程中其他运行中的程序的影响,硬件的影响,JVM参数的影响等等。


=============================== 华丽的终止符 ================================

本文作者:kafka0102,转载文章请注明来源,谢谢!!
本文链接:http://www.kafka0102.com/2010/08/312.html


相关日志


留下评论

说明:评论需要审核通过才能显示