作者:kiritomoe

来源:Kirito的技术分享

前言

JMH (http://openjdk.java.net/projects/code-tools/jmh/) 是 Java Microbenchmark Harness(微基准测试)框架的缩写(2013年首次发布)。与其他众多测试框架相比,其特色优势在于它是由 Oracle 实现 JIT 的相同人员开发的。在此,我想特别提一下 Aleksey Shipilev (http://shipilev.net/)(JMH 的作者兼布道者)和他优秀的博客文章。

笔者花费了一个周末,将 Aleksey 大神的博客,特别是那些和 JMH 相关的文章通读了一遍,外加一部公开课视频 《"The Lesser of Two Evils" Story》 ,将自己的收获归纳在这篇文章中,文中不少图片都来自 Aleksey 公开课视频。

阅读本文前

本文没有花费专门的篇幅在文中介绍 JMH 的语法,如果你使用过 JMH,那当然最好,但如果没听过它,也不需要担心(跟我一周前的状态一样)。我会从 Java Developer 角度来谈谈一些常见的代码测试陷阱,分析他们和操作系统底层以及 Java 底层的关联性,并借助 JMH 来帮助大家摆脱这些陷阱。

通读本文,需要一些操作系统相关以及部分 JIT 的基础知识,如果遇到陌生的知识点,可以留意章节中的维基百科链接,以及笔者推荐的博客。

笔者能力有限,未能完全理解 JMH 解决的全部问题,如有错误以及疏漏欢迎留言与我交流。

初识 JMH

  • 测试精度

测试精度

上图给出了不同类型测试的耗时数量级,可以发现 JMH 可以达到微秒级别的的精度。

这样几个数量级的测试所面临的挑战也是不同的。

  • 毫秒级别的测试并不是很困难

  • 微秒级别的测试是具备挑战性的,但并非无法完成,JMH 就做到了

  • 纳秒级别的测试,目前还没有办法精准测试

  • 皮秒级别…Holy Shit

图解:

  • Linpack : Linpack benchmark 一类基础测试,度量系统的浮点计算能力

  • SPEC:Standard Performance Evaluation Corporation 工业界的测试标准组织

  • pipelining:系统总线通信的耗时

Benchmark 分类

测试在不同的维度可以分为很多类:集成测试,单元测试,API 测试,压力测试… 而 Benchmark 通常译为基准测试(性能测试)。你可以在很多开源框架的包层级中发现 Benchmark,用于阐释该框架的基准水平,从而量化其性能。

基准测试又可以细分为 :Micro benchmark,Kernels,Synthetic benchmark,Application benchmarks.etc.本文的主角便属于 Benchmark 的 Micro benchmark。

基础测试分类详细介绍 here (http://prof.ict.ac.cn/DComputing/uploads/2013/DC_1_3_benchmark.pdf)

motan中的benchmark

  • 为什么需要有 Benchmark

If you cannot measure it, you cannot improve it.

--Lord Kelvin

俗话说,没有实践就没有发言权,Benchmark 为应用提供了数据支持,是评价和比较方法好坏的基准,Benchmark 的准确性,多样性便显得尤为重要。

Benchmark 作为应用框架,产品的基准画像,存在统一的标准,避免了不同测评对象自说自话的尴尬,应用框架各自使用有利于自身场景的测评方式必然不可取,例如 Standard Performance Evaluation Corporation (SPEC) 即上文“测试精度”提到的词便是工业界的标准组织之一,JMH 的作者 Aleksey 也是其中的成员。

  • JMH 长这样

@Benchmarkpublic void measure() {    // this method was intentionally left blank.}

使用起来和单元测试一样的简单

它的测评结果

Benchmark                                Mode  Cnt           Score           Error  UnitsJMHSample_HelloWorld.measure  thrpt    5  3126699413.430 ± 179167212.838  ops/s
  • 为什么需要 JMH 测试

你可能会想,我用下面的方式来测试有什么不好?

long start = System.currentTimeMillis();measure();System.out.println(System.currentTimeMillis()-start);

难道 JMH 不是这么测试的吗?

@Benchmarkpublic void measure() {}

事实上,这是本文的核心问题,建议在阅读时时刻带着这样的疑问,为什么不使用第一种方式来测试。在下面的章节中,我将列举诸多的测试陷阱,他们都会为这个问题提供论据,这些陷阱会启发那些对“测试”不感冒的开发者。

  • 预热

在初识 JMH 小节的最后,花少量的篇幅来给 JMH 涉及的知识点开个头,介绍一个 Java 测试中比较老生常谈的话题 — 预热(warm up),它存在于下面所有的测试中。

«Warmup» = waiting for the transient responses to settle down

特别是在编写 Java 测试程序时,预热从来都是不可或缺的一环,它使得结果更加真实可信。

warmup plateaus

上图展示了一个样例测评程序随着迭代次数增多执行耗时变化的曲线,可以发现在 120 次迭代之后,性能才趋于最终稳定,这意味着:预热阶段需要有至少 120 次迭代,才能得到准确的基础测试报告。(JVM 初始化时的一些准备工作以及 JIT 优化是主要原因,但不是唯一原因)。需要被说明的事,JMH 的运行相对耗时,因为,预热被前置在每一个测评任务之前。

使用 JMH 解决 12 个测试陷阱

  • 陷阱1:死码消除



measureWrong 方法想要测试 Math.log 的性能,得到的结果和空方法 baseline 一致,而 measureRight 相比 measureWrong 多了一个 return,正确的得到了测试结果。

这是由于 JIT 擅长删除“无效”的代码,这给我们的测试带来了一些意外,当你意识到 DCE 现象后,应当有意识的去消费掉这些孤立的代码,例如 return。JMH 不会自动实施对冗余代码的消除。

死码消除这个概念很多人其实并不陌生,注释的代码,不可达的代码块,可达但不被使用的代码等等,我这里补充一些 Aleksey 提到的概念,用以阐释为何一般测试方法难以避免引用对象发生死码消除现象:

  1. Fast object combinator.

  2. Need to escape object to limit thread-local optimizations.

  3. Publishing the object ⇒ reference heap write ⇒ store barrier.

很绝望,个人水平有限,我没能 get 到这些点,只能原封不动地贴给大家看了。

JMH 提供了专门的 API — Blockhole 来避免死码消除问题。

@Benchmarkpublic void measureRight(Blackhole bh) {    bh.consume(Math.log(PI));}
  • 陷阱2:常量折叠与常量传播

常量折叠 (Constant folding) 是一个在编译时期简化常数的一个过程,常数在表示式中仅仅代表一个简单的数值,就像是整数 2,若是一个变数从未被修改也可作为常数,或者直接将一个变数被明确地被标注为常数,例如下面的描述:

  i = 320 * 200 * 32;

多数的现代编译器不会真的产生两个乘法的指令再将结果储存下来,取而代之的,他们会辨识出语句的结构,并在编译时期将数值计算出来(在这个例子,结果为 2,048,000)。

有些编译器,常数折叠会在初期就处理完,例如 Java 中的 final 关键字修饰的变量就会被特殊处理。而将常数折叠放在较后期的阶段的编译器,也相当常见。

private double x = Math.PI;

// 编译器会对 final 变量特殊处理 private final double wrongX = Math.PI;

@Benchmarkpublic double baseline() { // 2.220 ± 0.352 ns/op    return Math.PI;}

@Benchmarkpublic double measureWrong_1() { // 2.220 ± 0.352 ns/op    // 错误,结果可以被预测,会发生常量折叠    return Math.log(Math.PI);}

@Benchmarkpublic double measureWrong_2() { // 2.220 ± 0.352 ns/op    // 错误,结果可以被预测,会发生常量折叠    return Math.log(wrongX);}

@Benchmarkpublic double measureRight() { // 22.590 ± 2.636  ns/op    return Math.log(x);}

经过 JMH 可以验证这一点:只有最后的 measureRight 正确测试出了 Math.log 的性能,measureWrong_1,measureWrong_2 都受到了常量折叠的影响。

常数传播(Constant propagation) 是一个替代表示式中已知常数的过程,也是在编译时期进行,包含前述所定义,内建函数也适用于常数,以下列描述为例:

  int x = 14;  int y = 7 - x / 2;  return y * (28 / x + 2);

传播可以理解变量的替换,如果进行持续传播,上式会变成:

  int x = 14;  int y = 0;  return 0;
  • 陷阱3:永远不要在测试中写循环

这个陷阱对我们做日常测试时的影响也是巨大的,所以我直接将他作为了标题:永远不要在测试中写循环!

本节设计不少知识点,循环展开(loop unrolling),JIT & OSR 对循环的优化。对于前者循环展开的定义,建议读者直接查看 wiki 的定义,而对于后者 JIT & OSR 对循环的优化,推荐两篇 R 大的知乎回答:

循环长度的相同、循环体代码相同的两次for循环的执行时间相差了100倍?

OSR(On-Stack Replacement)是怎样的机制?

对于第一个回答,可以直接看答案,问题本身有待商榷;第二个回答,阐释了 OSR 都对循环做了哪些手脚。

测试一个耗时较短的方法,入门级程序员(不了解动态编译的同学)会这样写,通过循环放大,再求均值。

public class BadMicrobenchmark {    public static void main(String[] args) {        long startTime = System.nanoTime();        for (int i = 0; i < 10_000_000; i++) {            reps();        }        long endTime = System.nanoTime();        System.out.println("ns/op : " + (endTime - startTime));    }}

实际上,这段代码的结果是不可预测的,太多影响因子会干扰结果。原理暂时不表,通过 JMH 来看看几个测试方法,下面的 Benchmark 尝试对 reps 方法迭代不同的次数,想从中获得 reps 真实的性能。(注意,在 JMH 中使用循环也是不可取的,除非你是 Benchmark 方面的专家,否则在任何时候,你都不应该写循环)

int x = 1;int y = 2;

@Benchmarkpublic int measureRight() {    return (x + y);}

private int reps(int reps) {    int s = 0;    for (int i = 0; i < reps; i++) {        s += (x + y);    }    return s;}

@Benchmark@OperationsPerInvocation(1)public int measureWrong_1() {    return reps(1);}

@Benchmark@OperationsPerInvocation(10)public int measureWrong_10() {    return reps(10);}

@Benchmark@OperationsPerInvocation(100)public int measureWrong_100() {    return reps(100);}

@Benchmark@OperationsPerInvocation(1000)public int measureWrong_1000() {    return reps(1000);}

@Benchmark@OperationsPerInvocation(10000)public int measureWrong_10000() {    return reps(10000);}

@Benchmark@OperationsPerInvocation(100000)public int measureWrong_100000() {    return reps(100000);}

结果如下:

Benchmark                               Mode  Cnt  Score   Error  UnitsJMHSample_11_Loops.measureRight         avgt    5  2.343 ± 0.199  ns/opJMHSample_11_Loops.measureWrong_1       avgt    5  2.358 ± 0.166  ns/opJMHSample_11_Loops.measureWrong_10      avgt    5  0.326 ± 0.354  ns/opJMHSample_11_Loops.measureWrong_100     avgt    5  0.032 ± 0.011  ns/opJMHSample_11_Loops.measureWrong_1000    avgt    5  0.025 ± 0.002  ns/opJMHSample_11_Loops.measureWrong_10000   avgt    5  0.022 ± 0.005  ns/opJMHSample_11_Loops.measureWrong_100000  avgt    5  0.019 ± 0.001  ns/op

如果不看事先给出的错误和正确的提示,上述的结果,你会选择相信哪一个?实际上跑分耗时从 2.358 随着迭代次数变大,降为了 0.019。手动测试循环的代码 BadMicrobenchmark 也存在同样的问题,实际上它没有做预热,效果只会比 JMH 测试循环更加不可信。

Aleksey 在视频中给出结论:假设单词迭代的耗时是

JAVA拾遗 — JMH与8个代码陷阱相关推荐

  1. JAVA拾遗 — JMH与8个测试陷阱

    前言 JMH 是 Java Microbenchmark Harness(微基准测试)框架的缩写(2013年首次发布).与其他众多测试框架相比,其特色优势在于它是由 Oracle 实现 JIT 的相同 ...

  2. java短_Java中的最短代码和最低延迟

    如何编写以最快速度执行的代码,同时仍将编码保持在最低限度? 最短代码和最低延迟 谁能编写具有最低延迟的最短Java代码,以及使用了哪些工具? 更具体地说,目标是开发一个Java应用程序,使用通用解决方 ...

  3. java kiwi_【Java拾遗】不可不知的 Java 序列化

    [Java拾遗]不可不知的 Java 序列化 前言 在程序运行的生命周期中,序列化与反序列化的操作,几乎无时无刻不在发生着.对于任何一门语言来说,不管它是编译型还是解释型,只要它需要通讯或者持久化时, ...

  4. JAVA 拾遗 --Future 模式与 Promise 模式

    JAVA 拾遗 --Future 模式与 Promise 模式 写这篇文章的动机,是缘起于微信闲聊群的一场讨论,粗略整理下,主要涉及了以下几个具体的问题: 同步,异步,阻塞,非阻塞的关联及区别. JA ...

  5. 【JAVA拾遗】Java8新特性合辑

    [JAVA拾遗]Java8新特性合辑 文章目录 [JAVA拾遗]Java8新特性合辑 0. 逼逼 [--/--]126 Lambda Expressions & Virtual Extensi ...

  6. 0-1背包 java_0-1背包问题,java的动态规划如题,代码如下public

    该楼层疑似违规已被系统折叠 隐藏此楼查看此楼 0-1背包问题,java的动态规划 如题,代码如下 public class dongtaiguihua01 { public static void m ...

  7. 连连看+php,java基于swing实现的连连看代码

    本文实例讲述了java基于swing实现连连看代码.分享给大家供大家参考. 主要功能代码如下:package llkan; import javax.swing.*; import java.awt. ...

  8. Java NIO原理 图文分析及代码实现

    最近在分析hadoop的RPC(Remote Procedure Call Protocol ,远程过程调用协议,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议.可以参考: ...

  9. Java拾遗:001 - 重写 equals 和 hashCode 方法

    2019独角兽企业重金招聘Python工程师标准>>> 重写equals方法 在Java中Object类是一个具体类,但它设计的主要目的是为了扩展,所以它的所有非final方法,都被 ...

最新文章

  1. Java 对象初始化的过程介绍
  2. 慎用url重写(转)
  3. 10分钟了解JSON Web令牌(JWT)
  4. 8597 石子划分问题 dpdp,只考虑第一次即可
  5. 机器学习Tensorflow基于MNIST数据集识别自己的手写数字(读取和测试自己的模型)
  6. python vbscript_将VBScript转换为Python
  7. 关于 LDTP 操纵 windows 控件。
  8. 19-6/24作业: 将一个double类型的小数,按照四舍五入保留两位小数
  9. Repeater 控件的嵌套使用
  10. 如何服务器备份到移动硬盘,数据安全第一!威联通如何外接硬盘备份和同步
  11. BAT等大厂年薪30W+面试清单:JVM\MySQL\设计模式\分布式\微服务
  12. 利用java程序实现文件加密
  13. 苹果计算机格式化磁盘,如何格式化Mac电脑硬盘_给Mac电脑格式化硬盘的方法
  14. ICP与IP备案管理系统常见问题总结(FAQ)
  15. 12、计算机如何实现开根号?
  16. 元柚话TK:海外抖音TikTok+独立站如何搭建?
  17. LabVIEW如何打开Acrobat PDF文件
  18. 《2021大数据产业年度趋势人物》榜重磅发布丨金猿奖
  19. 如何在 Windows 中重新安装或修复 Internet Explorer
  20. Python加密有敏感信息的Word/Excel等文件

热门文章

  1. Kronecker积及其等式性质
  2. 三极管与恒流源充放电电路
  3. plt.imshow()无法显示两站图片?
  4. AVR单片机开发1——IO口的输入和输出
  5. python自动读取excel文件邮箱列表,自动批量发送邮件项目(附使用方法+代码)
  6. 英才计划计算机潜质测评试题,opq(opq管理潜质测评试题)
  7. 国产麒麟系统PXE安装-UEFI引导
  8. 【树莓派】配置无线网络(wifi)
  9. BGP选路规则(实验做的有点乱)
  10. 2022国赛数学建模思路 - 案例:集成算法AdaBoost