Java 微基准测试框架 JMH
JMH 介绍
JMH 是一个 Java 工具,用于构建、运行和分析用 Java 和其他针对 JVM 语言编写的微基准测试。
依赖包
- maven
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.35</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.35</version>
</dependency>
- gradle
implementation group: 'org.openjdk.jmh', name: 'jmh-core', version: '1.32'
implementation group: 'org.openjdk.jmh', name: 'jmh-generator-annprocess', version: '1.32'
使用 @Benchmark
注解进行测试
使用 @Benchmark
注解可以指定需要进行测试的方法。Runner
是 JMH 的入口,Options
是它的入口参数,通过 OptionsBuilder
生成。我们可以在 Options
中对测试进行各项配置。指定类名作为 OptionsBuilder::include
的参数即可设置需要进行测试的类。
package com.example;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
public class Example_01_HelloJMH {
@Benchmark
public String sayHello() {
return "HELLO JMH!";
}
public static void main(String[] args) throws RunnerException {
Options options = new OptionsBuilder()
.include(Example_01_HelloJMH.class.getSimpleName())
.build();
new Runner(options).run();
}
}
使用 @BenchmarkMode 指定测试模式
使用 @BenchmarkMode
注解可以指定测试模式,参数是 Mode
类型数组。
@Benchmark
@BenchmarkMode(Mode.Throughput)
public void measureThroughput() throws InterruptedException {
/* 仅测试吞吐量 */
TimeUnit.MILLISECONDS.sleep(100);
}
@Benchmark
@BenchmarkMode({Mode.Throughput, Mode.AverageTime, Mode.SampleTime})
public void measureMultiple() throws InterruptedException {
/* 测试吞吐量、平均时间和抽样时间 */
TimeUnit.MILLISECONDS.sleep(100);
}
@Benchmark
@BenchmarkMode(Mode.All)
public void measureAll() throws InterruptedException {
/* 测试所有,即吞吐量、平均时间、抽样时间和启动时间 */
TimeUnit.MILLISECONDS.sleep(100);
}
Mode
枚举类枚举了 JMH 的测试模式,分别为:
模式 | 介绍 | 单位 |
---|---|---|
Throughout | 吞吐量,一段时间内程序的执行次数 | op/time |
AverageTime | 平均时间,执行程序的平均耗时 | time/op |
SampleTime | 执行时间随机取样,输出执行时间的结果分布 | time/op |
SingleShotTime | 运行一次,测试冷启动时间 | time/op |
All | 所有模式 | / |
单位中的 op 代表的是一次操作,默认一次操作指的是执行一次测试方法。但是我们可以指定调用多少次测试方法算作一次操作。在 JMH 中称作操作中的批处理次数,例如我们可以设置执行五次测试方法算作一次操作。
使用 @Measurement
指定测试次数
@Measurement
注解可作用于类或者方法上,用于指定测试的次数、时间和批处理数量,参数为:
iterations
:测量次数,默认是 5 次。time
:单次测量持续时间,默认是 10。timeUnit
:时间单位,指定 time 的单位,默认是秒。batchSize
:每次操作的批处理次数,默认是 1,即调用一次测试方法算作一次操作。
四个参数都可以在Options
中单独指定,优先级是:类 < 方法 <Options
。
package com.example;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.concurrent.TimeUnit;
@Measurement(iterations = 3, time = 500, timeUnit = TimeUnit.MILLISECONDS, batchSize = 1)
public class Example_03_Measurement {
public static void main(String[] args) throws RunnerException {
Options options = new OptionsBuilder()
.include(Example_03_Measurement.class.getSimpleName())
.build();
new Runner(options).run();
}
@Benchmark
public String hello01() {
return "Java";
}
@Benchmark
@Measurement(batchSize = 10)
public String hello02() {
return "Java";
}
@Benchmark
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS, batchSize = 1)
public String hello03() {
return "Java";
}
}
使用 @WarmUp 指定预热次数
由于 JVM 会使用 JIT 即时编译器对热点代码进行编译,因此同一份代码可能由于执行次数的增加而导致执行时间差异太大,因此我们可以让代码先预热几轮,预热时间不算入测量计时。@WarmUp 的使用和 @Measurement 一致。
使用 @Fork 设置测试进程数
使用 @Thread 设置执行使用的线程数
避免 JIT 优化
第一点就是使用 @WarmUp 对代码进行预热,防止先后执行的时间差异。但这仅仅只是解决时间先后存在的问题,JIT 还存在着其他非常多的优化手段,这是 JVM 牛逼的地方,但却是 JMH 难受的地方。
JIT 优化技术列表:PerformanceTacticIndex - PerformanceTacticIndex - OpenJDK Wiki (java.net)
防止无用代码消除
对于一些 JIT 判定没有意义的代码,JIT 在执行时会直接将之删除,如:
@Benchmark
public void noCode() {}
@Benchmark
public void doSth() {
double a = Math.sqrt(x);
}
二者执行结果对比为:
Benchmark Mode Cnt Score Error Units
Example_00_DeadCodeElimination.doSth thrpt 15 4048.484 ± 129.437 ops/us
Example_00_DeadCodeElimination.noCode thrpt 15 4117.765 ± 94.406 ops/us
二者执行结果非常相近,Math::sqrt
操作的消耗可是非常高的,但此处由于 a 变量没有任何作用,因此 doSth
中的代码直接被消除掉了,此时只需要将 a 作为返回值或者使用 jmh
提供的 Blackhole
对象接收就可以让代码不被消除:
@Benchmark
public void noCode() {}
@Benchmark
public void receiveByBlackHole(Blackhole blackhole) {
blackhole.consume(Math.sqrt(x));
}
@Benchmark
public double returnRes() {
return Math.sqrt(x);
}
执行结果:
Benchmark Mode Cnt Score Error Units
Example_00_DeadCodeElimination.noCode thrpt 15 4099.014 ± 110.712 ops/us
Example_00_DeadCodeElimination.receiveByBlackHole thrpt 15 209.898 ± 4.028 ops/us
Example_00_DeadCodeElimination.returnRes thrpt 15 209.714 ± 6.090 ops/us
使用 JMH 测试 SpringBoot
我们希望使用 JMH 测试 SpringBoot 的各个组件,比如我们编写的 Controller 或者 Service,由于 Spring Bean 会自动注入依赖,因此我们需要在测试程序中启动 SpringBoot。
package com.example.springbootjmhtest;
import com.example.springbootjmhtest.controller.TestController;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
public class SpringBootBenchMark {
private ConfigurableApplicationContext springContext;
private TestController testController;
public static void main(String[] args) throws RunnerException {
Options options = new OptionsBuilder()
.include(SpringBootBenchMark.class.getSimpleName())
.warmupIterations(3)
.measurementIterations(3)
.forks(3)
.build();
new Runner(options).run();
}
@Setup
public void setUp() {
// 启动 SpringBoot
springContext = SpringApplication.run(SpringbootJmhTestApplication.class);
// 加载 Bean,此时会自动注入 AService 和 BService
testController = springContext.getBean(TestController.class);
}
@TearDown
public void tearDown() {
springContext.close();
}
@Benchmark
public void testStringBuffer() {
// controller 调用 AService 的方法
testController.testAService();
}
@Benchmark
public void testStringBuilder() {
// controller 调用 BService 的方法
testController.testBService();
}
}
测试结果:
Benchmark Mode Cnt Score Error Units
SpringBootBenchMark.testStringBuffer avgt 9 1.455 ± 0.024 ms/op
SpringBootBenchMark.testStringBuilder avgt 9 1.358 ± 0.029 ms/op
真诚点赞 诚不我欺~