JMH简介
JMH(Java Microbenchmark Harness)是用于代码微基准测试的工具套件,主要是基于方法层面的基准测试,精度可以达到纳秒级。
JMH 比较典型的应用场景如下:
- 想准确地知道某个方法需要执行多长时间,以及执行时间和输入之间的相关性
- 对比接口不同实现在给定条件下的吞吐量
- 查看多少百分比的请求在多长时间内完成
下面我们以 fastjson 和 Jackson 为例使用 JMH 做基准测试。
引入依赖
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.28</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.28</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId> FastJson </artifactId>
<version>1.2.78</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.12.5</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.5</version>
</dependency>
编写基准测试用例
package jmh;
import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.results.format.ResultFormatType;
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;
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 5, time = 5)
@Threads(4)
@ Fork (1)
@State(value = Scope.Benchmark)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class JSONSerializerTest {
ObjectMapper objectMapper = new ObjectMapper();
@Param(value = {"10", "50", "100"})
private int times;
@Benchmark
public void testFastJson(Blackhole blackhole) {
for (int i = 0; i < times; i++) {
String user = JSON.toJSONString(new User());
blackhole.consume(user);
}
}
@Benchmark
public void testJackson(Blackhole blackhole) throws JsonProcessingException {
for (int i = 0; i < times; i++) {
String user = objectMapper.writeValueAsString(new User());
blackhole.consume(user);
}
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JSONSerializerTest.class.getSimpleName())
.result("result.json")
.resultFormat(ResultFormatType.JSON).build();
new Runner(opt).run();
}
}
其中需要测试的方法用 @Benchmark 注解标识,这些注解的具体含义将在下面介绍。
在 main() 函数中,首先对测试用例进行配置,使用 Builder 模式配置测试,将配置参数存入 Options 对象,并使用 Options 对象构造 Runner 启动测试。
执行基准测试
执行 main 方法后,稍等片刻,便能输出全部测试结果。接下来分别查看每个日志的信息:
# JMH version: 1.28
# VM version: JDK 1.8.0_231, Java HotSpot(TM) 64-Bit Server VM, 25.231-b11
# VM invoker: C:\Program Files\ java \jdk1.8.0_231\jre\bin\java.exe
# VM options: -javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2020.2.1\lib\idea_rt.jar=54352:C:\Program Files\JetBrains\IntelliJ IDEA 2020.2.1\bin -Dfile.encoding=UTF-8
# Blackhole mode: full + dont-inline hint
# Warmup: 3 iterations, 1 s each
# Measurement: 5 iterations, 5 s each
# Timeout: 10 min per iteration
# Threads: 4 threads, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: jmh.JSONSerializerTest.testFastJson
# Parameters: (times = 10)
可以看到JMH 版本、JVM 版本、线程数、测试方法、测试模式等测试参数信息。
# Warmup Iteration 1: 2762.740 ±(99.9%) 985.723 ns/op
# Warmup Iteration 2: 2821.324 ±(99.9%) 273.518 ns/op
# Warmup Iteration 3: 2854.380 ±(99.9%) 182.686 ns/op
此部分为每一次预热测试的性能指标,预热测试不会作为最终的统计结果。预热的目的是 让 JVM 对被测代码进行足够多的优化 ,比如,在预热后,被测代码应该得到了充分的 JIT 编译和优化。
Iteration 1: 2775.728 ±(99.9%) 283.540 ns/op
Iteration 2: 2651.981 ±(99.9%) 150.255 ns/op
Iteration 3: 2668.217 ±(99.9%) 176.983 ns/op
Iteration 4: 2554.293 ±(99.9%) 181.387 ns/op
Iteration 5: 2616.313 ±(99.9%) 149.811 ns/op
Result "jmh.JSONSerializerTest.testFastJson":
2653.306 ±(99.9%) 312.730 ns/op [Average]
(min, avg, max) = (2554.293, 2653.306, 2775.728), stdev = 81.215
CI (99.9%): [2340.577, 2966.036] (assumes normal distribution)
该部分显示测量迭代的情况,每一次迭代都显示了当前的执行速率,即一个操作所花费的时间。在进行 5 次迭代后,进行统计,在本例中, times 为 10 的情况下 testFastJson 方法的平均执行耗时为 2653.306 ns ,误差为 81.215 ns 。
最后的测试对比结果如下:
Benchmark (times) Mode Cnt Score Error Units
JSONSerializerTest.testFastJson 10 avgt 5 2653.306 ± 312.730 ns/op
JSONSerializerTest.testFastJson 50 avgt 5 13242.378 ± 479.027 ns/op
JSONSerializerTest.testFastJson 100 avgt 5 25789.098 ± 1017.915 ns/op
JSONSerializerTest.testJackson 10 avgt 5 3801.418 ± 476.998 ns/op
JSONSerializerTest.testJackson 50 avgt 5 18727.014 ± 531.329 ns/op
JSONSerializerTest.testJackson 100 avgt 5 38515.980 ± 1700.508 ns/op
通过测试结果可知,FastJson 还是比 Jackson 快了许多的。
JMH基础介绍
1、@BenchmarkMode
JMH 可以在不同模式下运行您的基准测试。JMH 提供以下基准测试模式:
Throughput( 吞吐量) | 测量每秒的操作数,这意味着您的基准测试方法每秒可以执行的次数。 |
AverageTime( 平均时间) | 测量执行基准方法(单次执行)所需的平均时间。 |
SampleTime( 采样时间) | 随机取样,最后输出取样结果的分布。 |
SingleShotTime( 单次时间) | 测量单个基准方法执行运行所需的时间。一般用于测试在冷启动(没有 JVM 预热)下的性能。 |
ALL | 以上所有措施。 |
2、@OutputTimeUnit
使用java.util.concurrent.TimeUnit作为参数进行使用。 包括以下 时间单位 :
- NANOSECONDS
- MICROSECONDS
- MILLISECONDS
- SECONDS
- MINUTES
- HOURS
- DAYS
3、@State
JMH 提供了可以重用状态对象的不同“范围”。状态范围在@State 注释的参数中指定。在上面的例子中,选择的范围是 Scope. Benchmark
Scope 类包含下列范围的常数:
Thread | 运行基准测试的每个线程都将创建自己的状态对象实例。 |
Group | 运行基准测试的每个线程组将创建自己的状态对象实例。 |
Benchmark | 运行基准测试的所有线程共享相同的状态对象。 |
4、@Warmup
预热所需要配置的一些基本测试参数,可用于类或者方法上。一般前几次进行 程序测试 的时候都会比较慢,所以要让程序进行几轮预热,保证测试的准确性。参数如下所示:
- iterations:预热的次数
- time:每次预热的时间
- timeUnit:时间的单位,默认秒
- batchSize:批处理大小,每次操作调用几次方法
5、@Measurement
实际调用方法所需要配置的一些基本测试参数,可用于类或者方法上,参数和 @Warmup 相同。
6、@Threads
每个进程中的测试线程数,可用于类或者方法上。如上示例中,使用的测试线程数为 4。
7、@Fork
进行 fork 的次数,可用于类或者方法上。如果 fork 数是 2 的话,则 JMH 会 fork 出两个进程来进行测试。
8、@Param
指定某项参数的多种情况,特别适合用来测试一个函数在不同的参数输入的情况下的性能,只能作用在字段上,使用该注解必须定义 @State 注解。
介绍完基础使用之后,我们再来看看两个经常要注意的情况。
常见问题
1、死代码消除
在运行微基准测试时,了解优化非常重要 。否则,它们可能会影响基准测试结果,并且对我们产生误导。
参考一个例子:
@Benchmark
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
public void doNothing() {
}
@Benchmark
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
public void objectCreation() {
new Object();
}
在运行测试之前,我们肯定猜测第二个测试代码块会消耗更多的时间,毕竟它多创建了一个对象。实际测试结果如下:
Benchmark Mode Cnt Score Error Units
BenchMark.doNothing avgt 40 0.609 ± 0.006 ns/op
BenchMark.objectCreation avgt 40 0.613 ± 0.007 ns/op
从测试结果可以看出,两个方法测试的结果几乎一致,所以我们如果不懂运行优化的话,可能就会错误的认为创建一个对象并不消耗时间。
产生上面的现象是由于JIT编译器会优化掉多余的代码造成的。因为 new Object() 没有在程序的任何地方被使用到,因此这段代码在编译时被消除了,所以导致测试结果跟第一个方法一样。
解决方法1:在程序中返回该对象。
@Benchmark
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
public Object pillarsOfCreation() {
return new Object();
}
解决方法2:使用 Blackhole 。
@Benchmark
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
public void blackHole(Blackhole blackhole) {
blackhole.consume(new Object());
}
那么正确的测试结果如下:
Benchmark Mode Cnt Score Error Units
BenchMark.blackHole avgt 20 4.126 ± 0.173 ns/op
BenchMark.doNothing avgt 20 0.639 ± 0.012 ns/op
BenchMark.objectCreation avgt 20 0.635 ± 0.011 ns/op
BenchMark.pillarsOfCreation avgt 20 4.061 ± 0.037 ns/op
那么就可以看出性能差异了。
2、常量折叠
参考示例:
@Benchmark
public double foldedLog() {
int x = 8;
return Math.log(x);
}
无论执行多少次,基于常量的计算都会返回完全相同的输出。 因此,JIT 编译器很有可能会用其结果替换为函数调用,如下:
@Benchmark
public double foldedLog() {
return 2.0794415416798357;
}
这种情况就称为常量折叠 。在这种情况下,常量折叠导致 Math.log 没有被 调用,因此会导致测试结果错误。
为了防止常量折叠,我们可以将常量封装在一个状态对象中:
@State(Scope.Benchmark)
public static class Log {
public int x = 8;
}
@Benchmark
public double log(Log input) {
return Math.log(input.x);
}
运行测试,结果如下:
Benchmark Mode Cnt Score Error Units
BenchMark.foldedLog thrpt 20 449313097.433 ± 11850214.900 ops/s
BenchMark.log thrpt 20 35317997.064 ± 604370.461 ops/s
总结:
本文介绍了JMH的使用,更多示例请参考官方: