最典型的场景就是你想知道两个功能相同的操作到底哪个性能比较好,通常会自己手撸一段代码,前后增加时间,然后对比多次执行的时间。这种做法比较原始,还要自己处理预热等问题。JMH提供了比较丰富的操作。且看如何使用。
使用示例
这个工具是JDK9 加入的,但是也可以直接使用Maven添加对应的依赖。
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>${jmh.version}</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>${jmh.version}</version>
<scope>provided</scope>
</dependency>
依赖添加完成以后即可编写性能测试程序
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Benchmark)
public class JmhApplication {
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JmhApplication.class.getSimpleName())
.forks(1)
.warmupIterations(2)
.measurementIterations(5)
.build();
new Runner(opt).run();
}
@Benchmark
public void stringAdd(){
String a = "";
for (int i = 0; i < 10; i++) {
a += i;
}
}
@Benchmark
public void stringBuilderAdd(){
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10; i++) {
sb.append(i);
}
}
}
如上述代码只需要在要进行基准测试的方法上增加Benchmark注解,即可进行基准测试。而类上的注解直接定义了基准测试的一些全局设置,如测试类型,时间单位等。
main方法里面使用Options类定义的预热运行测试方法的次数,以及运行基准测试运行方法的次数。接着构建Runner类运行基准测试。
// .... 省略其他输出
# Run progress: 50.00% complete, ETA 00:01:11
# Fork: 1 of 1
# Warmup Iteration 1: 23.463 ops/us
# Warmup Iteration 2: 19.687 ops/us
Iteration 1: 18.304 ops/us
Iteration 2: 20.687 ops/us
Iteration 3: 20.641 ops/us
Iteration 4: 20.194 ops/us
Iteration 5: 20.112 ops/us
// .... 省略其他输出
Benchmark Mode Cnt Score Error Units
JmhApplication.stringAdd thrpt 5 5.711 ± 1.784 ops/us
JmhApplication.stringBuilderAdd thrpt 5 19.988 ± 3.757 ops/us
结果如上,可以看出进行了2次的预热,执行5次迭代,来计算结果。最终输出了Benchmark的结果。关注点是Score和对应的单位Units,表示每个时间单位执行的操作次数。即第一个方法每微秒执行5.711 ± 1.784次,第二个方法是每微秒执行19.988 ± 3.757 次。
基准测试类型
基准测试类型,由注解BenchmarkMode来指定,主要由五种选择,实际是4种。
Throughput 整体吞吐量,结果是单位时间的执行次数。
Benchmark Mode Cnt Score Error Units
JmhApplication.stringAdd thrpt 5 5.711 ± 1.784 ops/us
JmhApplication.stringBuilderAdd thrpt 5 19.988 ± 3.757 ops/us
AverageTime 调用的平均时间,结果是执行一次所需要的时间。
Benchmark Mode Cnt Score Error Units
JmhApplication.stringAdd avgt 5 0.150 ± 0.002 us/op
JmhApplication.stringBuilderAdd avgt 5 0.049 ± 0.005 us/op
SampleTime 随机取样,最后输出取样结果的分布
Benchmark Mode Cnt Score Error Units
stringAdd sample 1264158 0.202 ± 0.019 us/op
stringAdd:stringAdd·p0.00 sample 0.100 us/op
stringAdd:stringAdd·p0.50 sample 0.200 us/op
stringAdd:stringAdd·p0.90 sample 0.200 us/op
stringAdd:stringAdd·p0.95 sample 0.200 us/op
stringAdd:stringAdd·p0.99 sample 0.300 us/op
stringAdd:stringAdd·p0.999 sample 2.200 us/op
stringAdd:stringAdd·p0.9999 sample 31.907 us/op
stringAdd:stringAdd·p1.00 sample 3895.296 us/op
stringBuilderAdd sample 1968851 0.082 ± 0.008 us/op
stringBuilderAdd:stringBuilderAdd·p0.00 sample ≈ 0 us/op
stringBuilderAdd:stringBuilderAdd·p0.50 sample 0.100 us/op
stringBuilderAdd:stringBuilderAdd·p0.90 sample 0.100 us/op
stringBuilderAdd:stringBuilderAdd·p0.95 sample 0.100 us/op
stringBuilderAdd:stringBuilderAdd·p0.99 sample 0.100 us/op
stringBuilderAdd:stringBuilderAdd·p0.999 sample 0.300 us/op
stringBuilderAdd:stringBuilderAdd·p0.9999 sample 10.896 us/op
stringBuilderAdd:stringBuilderAdd·p1.00 sample 3563.520 us/op
SingleShotTime 以上模式都是默认一次 iteration 是 1秒,唯有 SingleShotTime 是只运行一次。往往同时把 warmup 次数设为0,用于测试冷启动时的性能。
// 预热修改为0,执行的次数修改为1
Options opt = new OptionsBuilder()
.include(JmhApplication.class.getSimpleName())
.forks(1)
.warmupIterations(0)
.measurementIterations(1)
.build();
// 结果如下
Benchmark Mode Cnt Score Error Units
JmhApplication.stringAdd ss 32612.100 us/op
JmhApplication.stringBuilderAdd ss 18.500 us/op
All 运行全部以上的基准测试模式
其中最常用的是Throughput和AverageTime模式,其他的在正常场景基本上可以略过。
工具使用起来比较方便,功能也是挺多,此处不逐个介绍。一个很大的缺点是文档比较少,官方有完整的示例代码,可以直接阅读和执行来学习如何使用。