JMH Java方法性能优化基准测试

192 阅读3分钟

本文正在参与 “性能优化实战记录”话题征文活动。

JMH,即(Java Microbenchmark Harness),是专门用于JAVA代码微基准测试和优化的工具套件。何谓Micro Benchmark呢?简单的来说就是基于方法层面的基准测试,精度可以达到微秒级。当你定位到热点方法,希望进一步优化方法性能的时候,就可以使用JMH对优化的结果进行量化的分析。

基准测试:是指通过设计科学的测试方法、测试工具和测试系统,实现对一类测试对象的某项性能指标进行定量的和可对比的测试。比如鲁大师、安兔兔,都是按一定的基准或者在特定条件下去测试某一对象的的性能,比如显卡、IO、CPU之类的。

JMH比较典型的应用场景有:

  1. 想准确的知道某个方法需要执行多长时间,以及执行时间和输入之间的相关性。
  2. 对比接口不同实现在给定条件下的吞吐量,找到最优实现。
  3. 查看多少百分比的请求在多长时间内完成。

1 JMH入门案例

1.1 Maven依赖

JMH是 被作为JDK9而自带的,但是我们可以通过导入相关依赖或者jar包来使用。

<!--JMH-->
<!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-core -->
<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-core</artifactId>
    <version>1.21</version>
</dependency>
<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-generator-annprocess</artifactId>
    <version>1.21</version>
</dependency>

1.2 程序编写

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class HelloJMH {
    /**
     * 字符串拼接StringBuilder基准测试
     */
    @Benchmark
    public void testStringBuilder() {
        StringBuilder str = new StringBuilder();
        for (int i = 0; i < 1000; i++) {
            str.append(i);
        }
        String s = str.toString();
    }
    /**
     * 字符串拼接直接相加基准测试
     */
    @Benchmark
    public void testStringAdd() {
        String str = "";
        for (int i = 0; i < 1000; i++) {
            str = str + i;
        }
    }

    public static void main(String[] args) throws RunnerException {
        Options options = new OptionsBuilder()
                .include(HelloJMH.class.getSimpleName())  //包含的方法
                .forks(1)   //分出几个进程单独测试
                .build();
        new Runner(options).run();
    }
}

@Benchmark注解表示该方法是需要进行benchmark测试的方法。

@BenchmarkMode表示JMH测量方式和角度,本次是测量平均时间。

@OutputTimeUnit表示benchmark 结果所使用的时间单位,可用于类或者方法注解,使用java.util.concurrent.TimeUnit中的标准时间单位。

在 Main 方法中,通过Runner 类去运行Options 实例即可。官方提供了一个OptionsBuilder对象去流式构建。OptionsBuilder的其他配置信息在下面讲。

1.3 测试结果

1)	# JMH version: 1.21
2)	# VM version: JDK 1.8.0_144, Java HotSpot(TM) 64-Bit Server VM, 25.144-b01
3)	# VM invoker: C:\Program Files\Java\jdk1.8.0_144\jre\bin\java.exe
4)	# VM options: -javaagent:D:\soft\IntelliJ IDEA 2019.3\lib\idea_rt.jar=61956:D:\soft\IntelliJ IDEA 2019.3\bin -Dfile.encoding=UTF-8
5)	# Warmup: 5 iterations, 10 s each
6)	# Measurement: 5 iterations, 10 s each
7)	# Timeout: 10 min per iteration
8)	# Threads: 1 thread, will synchronize iterations
9)	# Benchmark mode: Average time, time/op
10)	# Benchmark: com.thread.test.JMH.HelloJMH.testStringAdd

11)	# Run progress: 0.00% complete, ETA 00:03:20
12)	# Fork: 1 of 1
13)	# Warmup Iteration   1: 506360.123 ns/op
14)	# Warmup Iteration   2: 460295.578 ns/op
15)	# Warmup Iteration   3: 492550.630 ns/op
16)	# Warmup Iteration   4: 482141.558 ns/op
17)	# Warmup Iteration   5: 469897.660 ns/op
18)	Iteration   1: 443427.726 ns/op
19)	Iteration   2: 456970.538 ns/op
20)	Iteration   3: 440686.491 ns/op
21)	Iteration   4: 451894.998 ns/op
22)	Iteration   5: 432889.165 ns/op


23)	Result "com.thread.test.JMH.HelloJMH.testStringAdd":
a)	445173.784 ±(99.9%) 36450.901 ns/op [Average]
b)	(min, avg, max) = (432889.165, 445173.784, 456970.538), stdev = 9466.183
c)	CI (99.9%): [408722.883, 481624.685] (assumes normal distribution)


24)	# JMH version: 1.21
25)	# VM version: JDK 1.8.0_144, Java HotSpot(TM) 64-Bit Server VM, 25.144-b01
26)	# VM invoker: C:\Program Files\Java\jdk1.8.0_144\jre\bin\java.exe
27)	# VM options: -javaagent:D:\soft\IntelliJ IDEA 2019.3\lib\idea_rt.jar=61956:D:\soft\IntelliJ IDEA 2019.3\bin -Dfile.encoding=UTF-8
28)	# Warmup: 5 iterations, 10 s each    //预热次数
29)	# Measurement: 5 iterations, 10 s each   //度量次数
30)	# Timeout: 10 min per iteration
31)	# Threads: 1 thread, will synchronize iterations
32)	# Benchmark mode: Average time, time/op
33)	# Benchmark: com.thread.test.JMH.HelloJMH.testStringBuilder

34)	# Run progress: 50.00% complete, ETA 00:01:40
35)	# Fork: 1 of 1
36)	# Warmup Iteration   1: 10372.126 ns/op
37)	# Warmup Iteration   2: 10301.755 ns/op
38)	# Warmup Iteration   3: 10006.275 ns/op
39)	# Warmup Iteration   4: 9778.343 ns/op
40)	# Warmup Iteration   5: 9868.092 ns/op
41)	Iteration   1: 9641.269 ns/op
42)	Iteration   2: 10259.971 ns/op
43)	Iteration   3: 9844.944 ns/op
44)	Iteration   4: 9704.533 ns/op
45)	Iteration   5: 9711.980 ns/op


46)	Result "com.thread.test.JMH.HelloJMH.testStringBuilder":
a)	9832.539 ±(99.9%) 963.347 ns/op [Average]
b)	(min, avg, max) = (9641.269, 9832.539, 10259.971), stdev = 250.178
c)	CI (99.9%): [8869.193, 10795.886] (assumes normal distribution)


47)	# Run complete. Total time: 00:03:21

48)	REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
49)	why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
50)	experiments, perform baseline and negative tests that provide experimental control, make sure
51)	the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
52)	Do not assume the numbers tell you what you want them to tell.

53)	Benchmark                       Mode  Cnt       Score       Error  Units
54)	JMH.HelloJMH.testStringAdd      avgt    5  445173.784 ± 36450.901  ns/op
55)	JMH.HelloJMH.testStringBuilder  avgt    5    9832.539 ±   963.347  ns/op

解释:

第1-10行表示测试的基本信息,比如,使用的Java路径,预热代码的迭代次数,测量代码的迭代次数,使用的线程数量,测试的统计单位等。

从第13行开始显示了每次预热迭代的结果,预热迭代不会作为最终的统计结果。预热的目的是让Java虚拟机对被测代码进行足够多的优化,比如,在预热后被测代码应该得到了充分的JIT编译和优化。

从第18行开始显示每次基准测试迭代的结果,每一次迭代都显示了当前的执行速率,即一个操作所花费的时间。

在进行5次迭代后,进行统计,结果在Result后。Result第一段结果告诉了我们最大值、最小值、平均值的信息。第二段是最主要的信息。在本例中,第54、55行显示了testStringBuilder和testStringAdd函数的平均执行花费时间和误差时间。从结果可以看出,大量字符串拼接式时,使用StringBuilder效率更高。

BenchmarkModeCntScoreErrorUnits
基准测试执行的方法测试模式运行多少次分数错误单位

2 JMH的基本概念和配置

2.1 测试模式(Mode)

Mode表示JMH的测量方式和角度,共有4种,吞吐量和方法执行的平均时间是最为常用的统计方式。可通过@BenchmarkMode注解配置。

Throughput: 整体吞吐量, 表示1秒内可以执行多少次调用。如下:

1)	Benchmark                        Mode  Cnt   Score    Error   Units
2)	JMH.HelloJMH.testStringAdd      thrpt    510⁻⁵           ops/ns
3)	JMH.HelloJMH.testStringBuilder  thrpt    510⁻⁴           ops/ns

AverageTime: 调用的平均时间, 指每一次调用所需要的时间,即案例中的模式

SampleTime:随机取样,最后输出取样结果的分布,例如“99%的调用在XXX 毫秒以内,99.99%的调用在XXX 毫秒以内”。如下:

1)	Benchmark                                                   Mode      Cnt        Score      Error  Units
2)	JMH.HelloJMH.testStringAdd                                sample   110636   451524.056 ± 1674.469  ns/op
3)	JMH.HelloJMH.testStringAdd:testStringAdd·p0.00            sample            307712.000             ns/op
4)	JMH.HelloJMH.testStringAdd:testStringAdd·p0.50            sample            392192.000             ns/op
5)	JMH.HelloJMH.testStringAdd:testStringAdd·p0.90            sample            558080.000             ns/op
6)	JMH.HelloJMH.testStringAdd:testStringAdd·p0.95            sample            649216.000             ns/op
7)	JMH.HelloJMH.testStringAdd:testStringAdd·p0.99            sample           1337344.000             ns/op
8)	JMH.HelloJMH.testStringAdd:testStringAdd·p0.999           sample           2023424.000             ns/op
9)	JMH.HelloJMH.testStringAdd:testStringAdd·p0.9999          sample           2742493.594             ns/op
10)	JMH.HelloJMH.testStringAdd:testStringAdd·p1.00            sample           3420160.000             ns/op
11)	JMH.HelloJMH.testStringBuilder                            sample  1228587    10293.875 ±   39.332  ns/op
12)	JMH.HelloJMH.testStringBuilder:testStringBuilder·p0.00    sample              8688.000             ns/op
13)	JMH.HelloJMH.testStringBuilder:testStringBuilder·p0.50    sample              9600.000             ns/op
14)	JMH.HelloJMH.testStringBuilder:testStringBuilder·p0.90    sample             10592.000             ns/op
15)	JMH.HelloJMH.testStringBuilder:testStringBuilder·p0.95    sample             11600.000             ns/op
16)	JMH.HelloJMH.testStringBuilder:testStringBuilder·p0.99    sample             21280.000             ns/op
17)	JMH.HelloJMH.testStringBuilder:testStringBuilder·p0.999   sample             71552.000             ns/op
18)	JMH.HelloJMH.testStringBuilder:testStringBuilder·p0.9999  sample            695296.000             ns/op
19)	JMH.HelloJMH.testStringBuilder:testStringBuilder·p1.00    sample           2019328.000             ns/op

SingleShotTime: 以上模式都是默认一次Iteration是1秒,唯有SingleShotTime 只运行一次。往往同时把warmup 次数设为0, 用于测试冷启动时的性能。

All: 顾名思义,所有模式,这个在内部测试中常用。

2.2 迭代(Iteration)

迭代是JMH的一次测量的单位。在大部分测量模式下,一次迭代表示1秒。在这一秒内会不间断调用被测方法,并采样计算吞吐量、平均时间等。

2.3 预热(Warmup)

Warmup 是指在实际进行 benchmark 前先进行预热的行为。

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

由于Java 虚拟机的JIT 的存在,同一个方法在JIT编译前后的时间将会不同。通常只考虑方法在JIT编译后的性能。使用 -Xint 参数可以关闭JIT优化。

2.4 状态(State)

@State注解,作用在类上。通过State 可以指定一个对象的作用范围,范围主要有三种:

  1. Scope.Thread:默认的State,每个测试线程分配一个实例,也就是一个对象只会被一个线程访问。在多线程池测试时,会为每一个线程生成一个对象;
  2. Scope.Benchmark:所有测试线程共享一个实例,用于测试有状态实例在多线程共享下的性能
  3. Scope.Group:每个线程组共享一个实例;

2.5 配置类(Options/OptionsBuilder)

在测试开始前, 首先要对测试进行配置。通常需要指定一些参数, 比如指定测试类(include) 、使用的进程个数(fork) 、预热迭代次数(warmuplterations) 。在配置启动测试时, 需要使用配置类。

OptionsBuilder的常用方法及对应的注解形式如下:

方法名 参数 作用 对应注解
include 接受一个字符串表达式,表示需要测试的类和方法。 指定要运行的基准测试类和方法 -
exclude 接受一个字符串表达式,表示不需要测试的类和方法 指定不要运行的基准测试类方法 -
warmupIterations 预热的迭代次数 指定预热的迭代次数 @Warmup
warmupBatchSize 预热批量的大小 指定预热批量的大小 @Warmup
warmupForks 预热模式:INDI,BULK,BULK_INDI 指定预热模式 @Warmup
warmupMode 预热的模式 指定预热的模式 @Warmup
warmupTime 预热的时间 指定预热的时间 @Warmup
measurementIterations 测试的迭代次数 指定测试的迭代次数 @Measurement
measurementBatchSize 测试批量的大小 指定测试批量的大小 @Measurement
measurementTime 测试的时间 指定测试的时间 @Measurement
mode 测试模式: Throughput(吞吐量), AverageTime(平均时间),SampleTime(在测试中,随机进行采样执行的时间),SingleShotTime(在每次执行中计算耗时),All(所有) 指定测试模式 @BenchmarkMode--可用于类或者方法上
Fork 子进程数
threads 每个方法开启线程数量 多线程测试 @Threads,可用在方法或者类上

2.6 其他注解

@OutputTimeUnit:benchmark 结果所使用的时间单位,可用于类或者方法注解,使用java.util.concurrent.TimeUnit中的标准时间单位。

@Setup:方法注解,会在执行 benchmark 之前被执行,正如其名,主要用于初始化。

@TearDown:方法注解,与@Setup相对的,会在所有benchmark执行结束以后执行,主要用于资源的回收等。

@Param:成员注解,可以用来指定某项参数的多种情况。特别适合用来测试一个函数在不同的参数输入的情况下的性能。@Param注解接收一个String数组,在@setup方法执行前转化为为对应的数据类型。多个@Param注解的成员之间是乘积关系,譬如有两个用@Param注解的字段,第一个有5个值,第二个字段有2个值,那么每个测试方法会跑5*2=10次。

参考资料:

  1. 《实战Java高并发程序设计》

如果有什么不懂或者需要交流,可以留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!