如何在Java中编写正确的微基准测试?

976

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

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

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

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


6
Java 9可能提供一些微基准测试的功能:http://openjdk.java.net/jeps/230 - Raedwald
3
我认为JEP的目标是向JDK代码中添加一些微基准测试,但我不认为jmh会被包含在JDK中... - assylias
可能是重复问题:如何在Java中计时方法的执行时间? - Basil Bourque
2
@Raedwald 你好,来自未来。它没有通过 - Michael
显示剩余2条评论
11个回答

867
关于编写微基准测试的提示(来自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 编译,例如:Trouble$1::run @ 2 (41 bytes)。如果追求最佳性能,请优先选择服务器端而不是客户端,并选择常规编译而不是 OSR 编译。

规则 4:注意初始化效果。在计时阶段期间不要进行首次打印,因为打印会加载和初始化类。除非您专门测试类加载(在这种情况下仅加载测试类),否则不要在热身阶段之外加载新类(或最终报告阶段)。规则 2 是针对此类效果的第一道防线。

规则5:注意去优化和重新编译效应。在计时阶段中,不要首次使用任何代码路径,因为编译器可能根据早先的乐观假设,认为该路径根本不会被使用而将其丢弃和重新编译。第2条规则是对抗此类效应的第一道防线。

规则6:使用适当的工具来读取编译器的心思,并且预料到它生成的代码会让你感到惊讶。在形成关于某些东西是更快还是更慢的理论之前,请自行检查代码。

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

规则8:在进行基准测试时,使用一个库可能更高效,并且已经为此目的进行了调试。例如JMHCaliperBill and Paul's Excellent UCSD Benchmarks for Java

5
这是一篇有趣的文章:http://www.ibm.com/developerworks/java/library/j-jtp12214/ - John Nilsson
162
如果您在大多数操作系统和JVM组合中使用System.currentTimeMillis(),精度通常为+/- 15毫秒。因此,除非您可以接受这种精度,否则请改用System.nanoTime()。请注意,System.nanoTime()方法可提供更高的精度。 - Scott Carey
5
一些来自JavaOne的论文:http://www.azulsystems.com/events/javaone_2009/session/2009_J1_Benchmark.pdf - bestsss
109
需要注意的是,System.nanoTime()并不能保证比System.currentTimeMillis()更准确。它只能保证至少与后者一样准确。但通常情况下,它确实比后者更准确。 - Gravity
50
使用 System.nanoTime() 而非 System.currentTimeMillis() 的主要原因在于前者保证单调递增。减去两个 currentTimeMillis 调用的返回值可能会导致负数结果,这可能是因为系统时间被某个 NTP 守护程序调整所致。 - Waldheinz
1
请注意,除非考虑到“协调省略”,否则您的基准测试结果将会误导。https://groups.google.com/forum/#!msg/mechanical-sympathy/icNZJejUHfE/BfDekfBEs_sJ - CaptainHastings

262

42
+1 可以作为已接受答案的规则8进行添加:规则8:由于许多事情可能会出错,您应该使用现有的库而不是尝试自己实现! - assylias
9
@Pangea 目前来说,jmh 可能比 Caliper 更优秀。详情请参见:https://groups.google.com/forum/#!msg/mechanical-sympathy/m4opvy4xq3U/7lY8x8SvHgwJ - assylias

97
重要的Java基准测试事项包括:
  • 在计时之前,通过多次运行代码来预热JIT
  • 确保您运行足够长的时间以便能够用秒或(更好的)十秒来测量结果
  • 虽然您不能在迭代之间调用 System.gc() ,但最好在测试之间运行它,以便每个测试都有一个“干净”的内存空间可以使用。 (是的, gc()更像是一个提示而不是保证,但根据我的经验,它非常有可能真正进行垃圾收集。)
  • 我喜欢显示迭代和时间,并且得分为时间/迭代,可以缩放,使“最佳”算法获得1.0的分数,其他算法以相对方式评分。这意味着您可以运行所有算法长时间,同时变化迭代数量和时间,但仍然获得可比较的结果。

我正在撰写一篇关于.NET基准测试框架设计的博客文章。 我有coupleearlier posts可能能够给您一些想法-当然并不是所有内容都适用,但其中的一些可能会有所帮助。


3
小小的挑剔:在我看来,“so that each test gets” 应该改为 “so that each test might get”,因为前者给人的印象是调用 gc 总是会释放未使用的内存。 - Sanjay T. Sharma
1
@SanjayT.Sharma:嗯,意图是它确实会这样做。虽然不能严格保证,但这实际上是一个相当强的提示。将进行编辑以使其更清晰。 - Jon Skeet
2
我不同意调用System.gc()。它只是一个提示,仅此而已。甚至没有“它将有希望做些什么”的说法。你永远不应该调用它。这是编程,不是艺术。 - gyorgyabraham
16
是的,这是一个提示 - 但通常我观察到人们会采纳它。如果你不喜欢使用System.gc(),那么你如何建议在一个测试中最小化由前面的测试创建的对象所导致的垃圾回收?我是实用主义者,而非教条主义者。 - Jon Skeet
11
@gyabraham:我不知道你所说的“great fallback”是什么意思。你能详细解释一下吗?并且,你有没有提出更好结果的建议?我明确说过这不是一个保证...... - Jon Skeet
显示剩余4条评论

50

1
请参阅此博客文章:http://psy-lob-saw.blogspot.com/2013/04/writing-java-micro-benchmarks-with-jmh.html,了解如何开始使用JMH的详细信息。 - Nitsan Wakart
1
FYI,JEP 230:Microbenchmark Suite 是一个基于 Java Microbenchmark Harness (JMH) 项目的 OpenJDK 提案。虽然 未能进入 Java 9,但可能会在以后加入。 - Basil Bourque

23

基准测试应该衡量时间/迭代次数还是迭代次数/时间,为什么?

这取决于你想要测试的内容

如果你关心延迟,那么使用时间/迭代次数,如果你关心吞吐量,那么使用迭代次数/时间。


16

确保您以某种方式使用在基准测试代码中计算的结果。否则,您的代码可能会被优化掉。


16

如果你正在尝试比较两个算法,请对每个算法进行至少两次基准测试,并交替顺序。例如:

for(i=1..n)
  alg1();
for(i=1..n)
  alg2();
for(i=1..n)
  alg2();
for(i=1..n)
  alg1();

我发现同一算法在不同的执行中有明显的差异(有时候可以达到5-10%)。

同时,确保n非常大,以便每个循环的运行时间至少为10秒左右。迭代次数越多,基准时间中的有效数字就越多,数据也更可靠。


6
改变顺序自然会影响运行时间。JVM 优化和缓存效应在这里起作用。最好的方法是让 JVM 优化“热启动”,多次运行并在不同的 JVM 中对每个测试进行基准测试。 - Mnementh
实际上,我认为对于大多数基准测试,您需要预热版本。如果您按照上面的建议运行10秒钟,则建议仅计算最后5秒钟 - 抛弃前5秒钟。请记住,Java在某个时候编译代码。 - Bill K

13

在Java编写微基准测试时有许多可能的陷阱。

第一:您必须计算各种需要随机时间的事件:垃圾回收,缓存效应(对于文件的操作系统和对于内存的CPU),IO等等。

第二:您不能信任非常短时间间隔的测量时间的准确性。

第三:JVM在执行时会优化您的代码。因此,同一JVM实例中的不同运行将变得越来越快。

我的建议:使您的基准测试运行几秒钟,这比毫秒级别的运行更可靠。预热JVM(即至少运行一次基准测试而不进行测量,以使JVM可以运行优化)。多次运行您的基准测试(可能5次),并取中位数值。在新的JVM实例中运行每个微基准测试(为每个基准测试调用新的Java),否则JVM的优化效果会影响后续运行的测试。不要执行在预热阶段未执行的事情(因为这可能触发类加载和重新编译)。


9
需要翻译的内容:

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

这是因为实现 A 在大部分基准测试运行中可能比实现 B 更快。但是,A 的差异性可能更高,因此与 B 相比,A 的测量性能优势将毫无意义。

因此,编写和正确运行微基准测试同样重要,正确分析测试结果也非常关键。


9
除了其他优秀的建议外,我还要注意以下几点:
对于某些CPU(例如带TurboBoost的Intel Core i5系列),温度(以及当前使用的核心数量、利用率百分比)会影响时钟速度。由于CPU是动态时钟,这可能会影响您的结果。例如,如果您有一个单线程应用程序,则最大时钟速度(带TurboBoost)高于使用所有内核的应用程序。因此,在某些系统上,这可能会干扰单线程和多线程性能的比较。请记住,温度和电压也会影响Turbo频率的维持时间。
也许更基本重要的方面是您可以直接控制的:确保您正在测量正确的内容!例如,如果您正在使用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");

1
是的,在定时区域内不要做无关的工作很重要,但你的第一个例子仍然是可以的。只有一个对println的调用,没有单独的标题行或其他东西,并且System.nanoTime()必须作为构造该调用的字符串参数的第一步进行评估。编译器在第一个示例中所能做的事情,在第二个示例中也同样可以做到,而且两者都没有鼓励它们在记录停止时间之前做额外的工作。 - Peter Cordes

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