如何用Java编写(并运行)正确的微基准测试?

我正在寻找一些代码示例和注释,以说明需要考虑的各种事情。

示例:基准测试应该测量时间/迭代还是迭代/时间,为什么?

相关:秒表基准测试是可接受的?


当前回答

Java HotSpot创建者提供的编写微基准测试的技巧:

规则0:阅读一篇关于jvm和微基准测试的著名论文。一个很好的例子是Brian Goetz, 2005年。不要对微基准测试期望过高;它们只测量JVM性能特征的有限范围。

规则1:始终包含一个运行测试内核的预热阶段,足以在计时阶段之前触发所有初始化和编译。(在预热阶段较少的迭代是可以的。经验法则是数万次内循环迭代。)

规则2:始终使用-XX:+PrintCompilation, -verbose:gc等运行,这样您就可以验证编译器和JVM的其他部分在计时阶段没有执行意外的工作。

规则2.1:在计时和预热阶段的开始和结束打印消息,这样您就可以验证在计时阶段没有来自规则2的输出。

规则3:注意-client和-server,以及OSR和常规编译之间的区别。-XX:+PrintCompilation标志报告OSR编译,使用一个at符号表示非初始入口点,例如:Trouble$1::run @ 2(41字节)。如果你追求最好的性能,更倾向于服务器而不是客户端,常规而不是OSR。

规则4:注意初始化效果。不要在计时阶段第一次打印,因为打印会加载和初始化类。不要在预热阶段(或最终报告阶段)之外加载新类,除非您专门测试类加载(在这种情况下只加载测试类)。规则2是你对抗这些影响的第一道防线。

规则5:注意反优化和重新编译的影响。不要在计时阶段第一次使用任何代码路径,因为编译器可能会丢弃并重新编译代码,这是基于先前的乐观假设,即该路径根本不会被使用。规则2是你对抗这些影响的第一道防线。

规则6:使用适当的工具来读取编译器的思想,并期待它生成的代码会让你大吃一惊。在形成是什么使代码更快或更慢的理论之前,自己检查代码。

规则7:减少测量中的噪声。在安静的机器上运行基准测试,并多次运行,丢弃异常值。使用-Xbatch将编译器与应用程序序列化,并考虑设置-XX:CICompilerCount=1以防止编译器与自身并行运行。尽量减少GC开销,设置Xmx(足够大)等于Xms,并在可用的情况下使用UseEpsilonGC。

规则8:使用一个库来进行基准测试,因为它可能更有效,并且已经为此目的进行了调试。例如JMH, Caliper或Bill和Paul的优秀UCSD Java基准。

其他回答

为了补充其他优秀的建议,我还会注意以下几点:

For some CPUs (e.g. Intel Core i5 range with TurboBoost), the temperature (and number of cores currently being used, as well as thier utilisation percent) affects the clock speed. Since CPUs are dynamically clocked, this can affect your results. For example, if you have a single-threaded application, the maximum clock speed (with TurboBoost) is higher than for an application using all cores. This can therefore interfere with comparisons of single and multi-threaded performance on some systems. Bear in mind that the temperature and volatages also affect how long Turbo frequency is maintained.

也许您可以直接控制的一个更根本的重要方面是:确保您在测量正确的东西!例如,如果您正在使用System.nanoTime()对特定代码进行基准测试,请将对赋值的调用放在有意义的位置,以避免测量您不感兴趣的内容。例如,不要做:

long startTime = System.nanoTime();
//code here...
System.out.println("Code took "+(System.nanoTime()-startTime)+"nano seconds");

问题是,当代码完成时,您不能立即得到结束时间。相反,试试下面的方法:

final long endTime, startTime = System.nanoTime();
//code here...
endTime = System.nanoTime();
System.out.println("Code took "+(endTime-startTime)+"nano seconds");

还应该注意的是,在比较不同的实现时,分析微基准测试的结果可能也很重要。因此,应进行显著性检验。

这是因为在基准测试的大多数运行过程中,实现A可能比实现B更快,但A也可能有更高的差异,因此与B相比,A的测量性能收益将没有任何意义。

因此正确编写和运行微基准测试也很重要,但正确分析它也很重要。

Java HotSpot创建者提供的编写微基准测试的技巧:

规则0:阅读一篇关于jvm和微基准测试的著名论文。一个很好的例子是Brian Goetz, 2005年。不要对微基准测试期望过高;它们只测量JVM性能特征的有限范围。

规则1:始终包含一个运行测试内核的预热阶段,足以在计时阶段之前触发所有初始化和编译。(在预热阶段较少的迭代是可以的。经验法则是数万次内循环迭代。)

规则2:始终使用-XX:+PrintCompilation, -verbose:gc等运行,这样您就可以验证编译器和JVM的其他部分在计时阶段没有执行意外的工作。

规则2.1:在计时和预热阶段的开始和结束打印消息,这样您就可以验证在计时阶段没有来自规则2的输出。

规则3:注意-client和-server,以及OSR和常规编译之间的区别。-XX:+PrintCompilation标志报告OSR编译,使用一个at符号表示非初始入口点,例如:Trouble$1::run @ 2(41字节)。如果你追求最好的性能,更倾向于服务器而不是客户端,常规而不是OSR。

规则4:注意初始化效果。不要在计时阶段第一次打印,因为打印会加载和初始化类。不要在预热阶段(或最终报告阶段)之外加载新类,除非您专门测试类加载(在这种情况下只加载测试类)。规则2是你对抗这些影响的第一道防线。

规则5:注意反优化和重新编译的影响。不要在计时阶段第一次使用任何代码路径,因为编译器可能会丢弃并重新编译代码,这是基于先前的乐观假设,即该路径根本不会被使用。规则2是你对抗这些影响的第一道防线。

规则6:使用适当的工具来读取编译器的思想,并期待它生成的代码会让你大吃一惊。在形成是什么使代码更快或更慢的理论之前,自己检查代码。

规则7:减少测量中的噪声。在安静的机器上运行基准测试,并多次运行,丢弃异常值。使用-Xbatch将编译器与应用程序序列化,并考虑设置-XX:CICompilerCount=1以防止编译器与自身并行运行。尽量减少GC开销,设置Xmx(足够大)等于Xms,并在可用的情况下使用UseEpsilonGC。

规则8:使用一个库来进行基准测试,因为它可能更有效,并且已经为此目的进行了调试。例如JMH, Caliper或Bill和Paul的优秀UCSD Java基准。

我知道这个问题已经被标记为已回答,但我想提到两个帮助我们编写微基准的库

来自谷歌的卡尺

入门教程

http://codingjunkie.net/micro-benchmarking-with-caliper/ http://vertexlabs.co.uk/blog/caliper

来自OpenJDK的JMH

入门教程

避免JVM上的基准测试陷阱 使用JMH进行Java微基准测试 JMH简介

jmh是最近添加到OpenJDK的,是由Oracle的一些性能工程师编写的。当然值得一看。

jmh是一个Java工具,用于构建、运行和分析用Java和其他针对JVM的语言编写的纳米/微/宏基准测试。

样本测试注释中隐藏着非常有趣的信息。

参见:

避免JVM上的基准测试陷阱 讨论jmh的主要优势。