专栏首页zhishengJava微基准测试框架JMH

Java微基准测试框架JMH

本文转自:https://www.xncoding.com/2018/01/07/java/jmh.html 作者:XiongNeng

JMH,即Java Microbenchmark Harness,这是专门用于进行代码的微基准测试的一套工具API。

JMH 由 OpenJDK/Oracle 里面那群开发了 Java 编译器的大牛们所开发 。何谓 Micro Benchmark 呢? 简单地说就是在 method 层面上的 benchmark,精度可以精确到微秒级。

Java的基准测试需要注意的几个点:

  • 测试前需要预热。
  • 防止无用代码进入测试方法中。
  • 并发测试。
  • 测试结果呈现。

比较典型的使用场景:

  1. 当你已经找出了热点函数,而需要对热点函数进行进一步的优化时,就可以使用 JMH 对优化的效果进行定量的分析。
  2. 想定量地知道某个函数需要执行多长时间,以及执行时间和输入 n 的相关性
  3. 一个函数有两种不同实现(例如JSON序列化/反序列化有Jackson和Gson实现),不知道哪种实现性能更好

尽管 JMH 是一个相当不错的 Micro Benchmark Framework,但很无奈的是网上能够找到的文档比较少,而官方也没有提供比较详细的文档,对使用造成了一定的障碍。 但是有个好消息是官方的 Code Sample 写得非常浅显易懂, 推荐在需要详细了解 JMH 的用法时可以通读一遍——本文则会介绍 JMH 最典型的用法和部分常用选项。

第一个例子

添加maven依赖

如果使用maven项目,只需要添加如下依赖:

 1<!-- JMH-->
 2<dependency>
 3    <groupId>org.openjdk.jmh</groupId>
 4    <artifactId>jmh-core</artifactId>
 5    <version>${jmh.version}</version>
 6</dependency>
 7<dependency>
 8    <groupId>org.openjdk.jmh</groupId>
 9    <artifactId>jmh-generator-annprocess</artifactId>
10    <version>${jmh.version}</version>
11    <scope>provided</scope>
12</dependency>

编写性能测试

接下来我写一个比较字符串连接操作的时候,直接使用字符串相加和使用StringBuilder的append方式的性能比较测试:

 1/**
 2 * 比较字符串直接相加和StringBuilder的效率
 3 */
 4@BenchmarkMode(Mode.Throughput)
 5@Warmup(iterations = 3)
 6@Measurement(iterations = 10, time = 5, timeUnit = TimeUnit.SECONDS)
 7@Threads(8)
 8@Fork(2)
 9@OutputTimeUnit(TimeUnit.MILLISECONDS)
10public class StringBuilderBenchmark {
11
12    @Benchmark
13    public void testStringAdd() {
14        String a = "";
15        for (int i = 0; i < 10; i++) {
16            a += i;
17        }
18        print(a);
19    }
20
21    @Benchmark
22    public void testStringBuilderAdd() {
23        StringBuilder sb = new StringBuilder();
24        for (int i = 0; i < 10; i++) {
25            sb.append(i);
26        }
27        print(sb.toString());
28    }
29
30    private void print(String a) {
31    }
32}

执行方式

这个代码里面有好多注解,你第一次见可能不知道什么意思。先不用管,我待会一一介绍。

我们来运行这个测试,运行JMH基准测试有多种方式,一个是生成jar文件执行, 一个是直接写main函数或写单元测试执行。

一般对于大型的测试,需要测试时间比较久,线程比较多的话,就需要去写好了丢到linux程序里执行, 不然本机执行很久时间什么都干不了了。

1mvn clean package
2java -jar target/benchmarks.jar

先编译打包之后,然后执行就可以了。当然在执行的时候可以输入-h参数来看帮助。

另外如果对于一些小的测试,比如我写的上面这个小例子,在IDE里面就可以完成了,丢到linux上去太麻烦。 这时候可以在里面添加一个main函数如下:

1public static void main(String[] args) throws RunnerException {
2    Options options = new OptionsBuilder()
3            .include(StringBuilderBenchmark.class.getSimpleName())
4            .output("E:/Benchmark.log")
5            .build();
6    new Runner(options).run();
7}

这里其实也比较简单,new个Options,然后传入要运行哪个测试,选择基准测试报告输出文件地址,然后通过Runner的run方法就可以跑起来了。

报告结果

我们跑一下这个基准测试,完成后打开E:/Benchmark.log,结果如下:

  1# JMH version: 1.20
  2# VM version: JDK 1.8.0_131, VM 25.131-b11
  3# VM invoker: C:\Program Files\Java\jdk1.8.0_131\jre\bin\java.exe
  4# VM options: -javaagent:E:\Program Files\JetBrains\IntelliJ IDEA 2017.3\lib\idea_rt.jar=62744:E:\Program Files\JetBrains\IntelliJ IDEA 2017.3\bin -Dfile.encoding=UTF-8
  5# Warmup: 3 iterations, 1 s each
  6# Measurement: 10 iterations, 5 s each
  7# Timeout: 10 min per iteration
  8# Threads: 16 threads, will synchronize iterations
  9# Benchmark mode: Throughput, ops/time
 10# Benchmark: com.xncoding.benchmark.string.StringBuilderBenchmark.testStringAdd
 11
 12# Run progress: 0.00% complete, ETA 00:03:32
 13# Fork: 1 of 2
 14# Warmup Iteration   1: 7332.410 ops/ms
 15# Warmup Iteration   2: 8758.506 ops/ms
 16# Warmup Iteration   3: 9078.783 ops/ms
 17Iteration   1: 8824.713 ops/ms
 18Iteration   2: 9084.977 ops/ms
 19Iteration   3: 9412.712 ops/ms
 20Iteration   4: 8843.631 ops/ms
 21Iteration   5: 9030.556 ops/ms
 22Iteration   6: 9090.677 ops/ms
 23Iteration   7: 9493.148 ops/ms
 24Iteration   8: 8664.593 ops/ms
 25Iteration   9: 8835.227 ops/ms
 26Iteration  10: 8570.212 ops/ms
 27
 28# Run progress: 25.00% complete, ETA 00:03:15
 29# Fork: 2 of 2
 30# Warmup Iteration   1: 5350.686 ops/ms
 31# Warmup Iteration   2: 8862.238 ops/ms
 32# Warmup Iteration   3: 8086.594 ops/ms
 33Iteration   1: 9105.306 ops/ms
 34Iteration   2: 8288.588 ops/ms
 35Iteration   3: 9307.902 ops/ms
 36Iteration   4: 9195.150 ops/ms
 37Iteration   5: 8715.555 ops/ms
 38Iteration   6: 9075.069 ops/ms
 39Iteration   7: 9041.037 ops/ms
 40Iteration   8: 9187.099 ops/ms
 41Iteration   9: 9145.134 ops/ms
 42Iteration  10: 9124.229 ops/ms
 43
 44
 45Result "com.xncoding.benchmark.string.StringBuilderBenchmark.testStringAdd":
 46  9001.776 ±(99.9%) 253.496 ops/ms [Average]
 47  (min, avg, max) = (8288.588, 9001.776, 9493.148), stdev = 291.926
 48  CI (99.9%): [8748.280, 9255.272] (assumes normal distribution)
 49
 50
 51# JMH version: 1.20
 52# VM version: JDK 1.8.0_131, VM 25.131-b11
 53# VM invoker: C:\Program Files\Java\jdk1.8.0_131\jre\bin\java.exe
 54# VM options: -javaagent:E:\Program Files\JetBrains\IntelliJ IDEA 2017.3\lib\idea_rt.jar=62744:E:\Program Files\JetBrains\IntelliJ IDEA 2017.3\bin -Dfile.encoding=UTF-8
 55# Warmup: 3 iterations, 1 s each
 56# Measurement: 10 iterations, 5 s each
 57# Timeout: 10 min per iteration
 58# Threads: 16 threads, will synchronize iterations
 59# Benchmark mode: Throughput, ops/time
 60# Benchmark: com.xncoding.benchmark.string.StringBuilderBenchmark.testStringBuilderAdd
 61
 62# Run progress: 50.00% complete, ETA 00:02:07
 63# Fork: 1 of 2
 64# Warmup Iteration   1: 27202.528 ops/ms
 65# Warmup Iteration   2: 26500.586 ops/ms
 66# Warmup Iteration   3: 27190.346 ops/ms
 67Iteration   1: 27891.257 ops/ms
 68Iteration   2: 28704.541 ops/ms
 69Iteration   3: 27785.951 ops/ms
 70Iteration   4: 26841.454 ops/ms
 71Iteration   5: 26024.288 ops/ms
 72Iteration   6: 25592.494 ops/ms
 73Iteration   7: 25626.875 ops/ms
 74Iteration   8: 25302.248 ops/ms
 75Iteration   9: 25519.780 ops/ms
 76Iteration  10: 25275.334 ops/ms
 77
 78# Run progress: 75.00% complete, ETA 00:01:02
 79# Fork: 2 of 2
 80# Warmup Iteration   1: 30376.008 ops/ms
 81# Warmup Iteration   2: 25131.064 ops/ms
 82# Warmup Iteration   3: 25622.342 ops/ms
 83Iteration   1: 25386.845 ops/ms
 84Iteration   2: 25825.139 ops/ms
 85Iteration   3: 26029.607 ops/ms
 86Iteration   4: 25531.748 ops/ms
 87Iteration   5: 25374.934 ops/ms
 88Iteration   6: 25204.530 ops/ms
 89Iteration   7: 22934.211 ops/ms
 90Iteration   8: 23907.677 ops/ms
 91Iteration   9: 24337.963 ops/ms
 92Iteration  10: 24660.626 ops/ms
 93
 94
 95Result "com.xncoding.benchmark.string.StringBuilderBenchmark.testStringBuilderAdd":
 96  25687.875 ±(99.9%) 1167.955 ops/ms [Average]
 97  (min, avg, max) = (22934.211, 25687.875, 28704.541), stdev = 1345.019
 98  CI (99.9%): [24519.920, 26855.830] (assumes normal distribution)
 99
100
101# Run complete. Total time: 00:04:08
102
103Benchmark                                     Mode  Cnt      Score      Error   Units
104StringBuilderBenchmark.testStringAdd         thrpt   20   9001.776 ±  253.496  ops/ms
105StringBuilderBenchmark.testStringBuilderAdd  thrpt   20  25687.875 ± 1167.955  ops/ms

仔细看,三大部分,第一部分是字符串用加号连接执行的结果,第二部分是StringBuilder执行的结果,第三部分就是两个的简单结果比较。这里注意我们forks传的2,所以每个测试有两个fork结果。

前两部分是一样的,简单说下。首先会写出每部分的一些参数设置,然后是预热迭代执行(Warmup Iteration), 然后是正常的迭代执行(Iteration),最后是结果(Result)。这些看看就好,我们最关注的就是第三部分, 其实也就是最终的结论。千万别看歪了,他输出的也确实很不爽,error那列其实没有内容,score的结果是xxx ± xxx,单位是每毫秒多少个操作。可以看到,StringBuilder的速度还确实是要比String进行文字叠加的效率好太多。

注解介绍

好了,当你对JMH有了一个基本认识后,现在来详细解释一下前面代码中的各个注解含义。

@BenchmarkMode

基准测试类型。这里选择的是Throughput也就是吞吐量。根据源码点进去,每种类型后面都有对应的解释,比较好理解,吞吐量会得到单位时间内可以进行的操作数。

  • Throughput: 整体吞吐量,例如“1秒内可以执行多少次调用”。
  • AverageTime: 调用的平均时间,例如“每次调用平均耗时xxx毫秒”。
  • SampleTime: 随机取样,最后输出取样结果的分布,例如“99%的调用在xxx毫秒以内,99.99%的调用在xxx毫秒以内”
  • SingleShotTime: 以上模式都是默认一次 iteration 是 1s,唯有 SingleShotTime 是只运行一次。往往同时把 warmup 次数设为0,用于测试冷启动时的性能。
  • All(“all”, “All benchmark modes”);

@Warmup

上面我们提到了,进行基准测试前需要进行预热。一般我们前几次进行程序测试的时候都会比较慢, 所以要让程序进行几轮预热,保证测试的准确性。其中的参数iterations也就非常好理解了,就是预热轮数。

为什么需要预热?因为 JVM 的 JIT 机制的存在,如果某个函数被调用多次之后,JVM 会尝试将其编译成为机器码从而提高执行速度。所以为了让 benchmark 的结果更加接近真实情况就需要进行预热。

@Measurement

度量,其实就是一些基本的测试参数。

  1. iterations 进行测试的轮次
  2. time 每轮进行的时长
  3. timeUnit 时长单位

都是一些基本的参数,可以根据具体情况调整。一般比较重的东西可以进行大量的测试,放到服务器上运行。

@Threads

每个进程中的测试线程,这个非常好理解,根据具体情况选择,一般为cpu乘以2。

@Fork

进行 fork 的次数。如果 fork 数是2的话,则 JMH 会 fork 出两个进程来进行测试。

@OutputTimeUnit

这个比较简单了,基准测试结果的时间类型。一般选择秒、毫秒、微秒。

@Benchmark

方法级注解,表示该方法是需要进行 benchmark 的对象,用法和 JUnit 的 @Test 类似。

@Param

属性级注解,@Param 可以用来指定某项参数的多种情况。特别适合用来测试一个函数在不同的参数输入的情况下的性能。

@Setup

方法级注解,这个注解的作用就是我们需要在测试之前进行一些准备工作,比如对一些数据的初始化之类的。

@TearDown

方法级注解,这个注解的作用就是我们需要在测试之后进行一些结束工作,比如关闭线程池,数据库连接等的,主要用于资源的回收等。

@State

当使用@Setup参数的时候,必须在类上加这个参数,不然会提示无法运行。

State 用于声明某个类是一个“状态”,然后接受一个 Scope 参数用来表示该状态的共享范围。 因为很多 benchmark 会需要一些表示状态的类,JMH 允许你把这些类以依赖注入的方式注入到 benchmark 函数里。Scope 主要分为三种。

  1. Thread: 该状态为每个线程独享。
  2. Group: 该状态为同一个组里面所有线程共享。
  3. Benchmark: 该状态在所有线程间共享。

关于State的用法,官方的 code sample 里有比较好的例子。

第二个例子

再来看一个更常规一点性能测试的例子,

计算 1 ~ n 之和,比较串行算法和并行算法的效率,看 n 在大约多少时并行算法开始超越串行算法

首先定义一个表示这两种实现的接口:

 1/**
 2 * Calculator
 3 *
 4 * @author XiongNeng
 5 * @version 1.0
 6 * @since 2018/1/7
 7 */
 8public interface Calculator {
 9    /**
10     * calculate sum of an integer array
11     *
12     * @param numbers
13     * @return
14     */
15    public long sum(int[] numbers);
16
17    /**
18     * shutdown pool or reclaim any related resources
19     */
20    public void shutdown();
21}

具体的两种实现代码我就不贴了,主要说明一下串行算法和并行算法实现原理:

  • 串行算法:使用 for-loop 来计算 n 个正整数之和。
  • 并行算法:将所需要计算的 n 个正整数分成 m 份,交给 m 个线程分别计算出和以后,再把它们的结果相加。

进行 benchmark 的代码如下:

 1/**
 2 * 自然数求和的串行和并行算法性能测试
 3 *
 4 * @author XiongNeng
 5 * @version 1.0
 6 * @since 2018/1/7
 7 */
 8@BenchmarkMode(Mode.AverageTime)
 9@OutputTimeUnit(TimeUnit.MICROSECONDS)
10@State(Scope.Benchmark)
11public class SecondBenchmark {
12    @Param({"10000", "100000", "1000000"})
13    private int length;
14
15    private int[] numbers;
16    private Calculator singleThreadCalc;
17    private Calculator multiThreadCalc;
18
19    public static void main(String[] args) throws Exception {
20        Options opt = new OptionsBuilder()
21                .include(SecondBenchmark.class.getSimpleName())
22                .forks(1)
23                .warmupIterations(5)
24                .measurementIterations(2)
25                .build();
26        Collection<RunResult> results =  new Runner(opt).run();
27        ResultExporter.exportResult("单线程与多线程求和性能", results, "length", "微秒");
28    }
29
30    @Benchmark
31    public long singleThreadBench() {
32        return singleThreadCalc.sum(numbers);
33    }
34
35    @Benchmark
36    public long multiThreadBench() {
37        return multiThreadCalc.sum(numbers);
38    }
39
40    @Setup
41    public void prepare() {
42        numbers = IntStream.rangeClosed(1, length).toArray();
43        singleThreadCalc = new SinglethreadCalculator();
44        multiThreadCalc = new MultithreadCalculator(Runtime.getRuntime().availableProcessors());
45    }
46
47    @TearDown
48    public void shutdown() {
49        singleThreadCalc.shutdown();
50        multiThreadCalc.shutdown();
51    }
52}

我在自己的笔记本电脑上跑下来的结果,总数在10000时并行算法不如串行算法, 总数达到100000时并行算法开始和串行算法接近,总数达到1000000时并行算法所耗时间约是串行算法的一半左右。

参考文章

  • Java使用JMH进行简单的基准测试Benchmark
  • Java 并发编程笔记:JMH 性能测试框架
  • JMH - Java Microbenchmark Harness

本文分享自微信公众号 - zhisheng(zhisheng_blog)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2018-12-22

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 高并发编程-volatile详解

    在介绍volatile之前,先简单了解一下Java内存模型。在Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏...

    JavaQ
  • 哦,这就是java的优雅停机?(实现及原理)

    其实优雅停机,就是在要关闭服务之前,不是立马全部关停,而是做好一些善后操作,比如:关闭线程、释放连接资源等。

    用户1655470
  • kotlin和java混合开发总结目录一、Kotlin集成步骤和注意事项(基于Kotlin1.3.0):二、Kotlin和Java文件互相转换三、Kotlin项目里面集成Java的module四、Ja

    声明:本文是作者AWeiLoveAndroid原创,版权归作者AWeiLoveAndroid所有,侵权必究。如若转发,请注明作者和来源地址!未经授权,严禁私自转...

    AWeiLoveAndroid
  • NA、Inf、NaN、NULL等值处理

    这几个都是R语言里面的特殊值,都是R的保留字(reserved words)。它们的意义分别为:

    用户1359560
  • 一位资深程序员大牛给予Java初学者的学习路线建议

    很多人问我如何学习Java的?能不能给点建议?今天我是打算来点干货,因此咱们就不说一些学习方法和技巧了,直接来谈每个阶段要学习的内容甚至是一些书籍。这一部分的内...

    范蠡
  • 关于JAVA你必须知道的那些事(一):概述

    第一次写文章,有点小紧张,不过没关系,因为我面对的都是小白。好了废话少说,直接开始吧。

    啃饼小白
  • Bytom Java版本离线签名

    Gitee地址:https://gitee.com/BytomBlockchain/bytom

    比原链Bytom
  • js的由弱变强之路,Flow为js添加编译过程

    javascript是一门弱类型语言, 所谓弱类型, 就是一个变量既可以被赋值字符串, 数字, 又可以被赋值数组, 对象, 弱类型的好处很多, 但也有缺点, 比...

    zhaoolee
  • RabbitMQ生产端消息可靠性投递方案分析

    导文: 1.什么是RabbitMQ 2.Java开发技术大杂烩(三)之电商项目优化、rabbitmq、Git、OSI、VIM、Intellj IDEA、HT...

    用户2032165
  • ### 0x01 C++ 资源大全

    关于 C++ 框架、库和资源的一些汇总列表,内容包括:标准库、Web应用框架、人工智能、数据库、图片处理、机器学习、日志、代码分析等。

    上善若水.夏

扫码关注云+社区

领取腾讯云代金券