JMH 快速入门 基准测试简介 什么是基准测试 基准测试是指通过设计科学的测试方法、测试工具和测试系统,实现对一类测试对象的某项性能指标进行定量的和可对比的测试。
现代软件常常都把高性能作为目标。那么,何为高性能,性能就是快,更快吗?显然,如果没有一个量化的标准,难以衡量性能的好坏。
不同的基准测试其具体内容和范围也存在很大的不同。如果是专业的性能工程师,更加熟悉的可能是类似 SPEC 提供的工业标准的系统级测试;而对于大多数 Java 开发者,更熟悉的则是范围相对较小、关注点更加细节的微基准测试(Micro-Benchmark)。何谓 Micro Benchmark 呢? 简单地说就是在 method 层面上的 benchmark,精度可以精确到 微秒级 。
何时需要微基准测试 微基准测试大多是 API 级别的性能测试。
微基准测试的适用场景:
如果开发公共类库、中间件,会被其他模块经常调用的 API。
对于性能,如响应延迟、吞吐量有严格要求的核心 API。
JMH 简介 JMH(即 Java Microbenchmark Harness) ,是目前主流的微基准测试框架。JMH 是由 Hotspot JVM 团队专家开发的,除了支持完整的基准测试过程,包括预热、运行、统计和报告等,还支持 Java 和其他 JVM 语言。更重要的是,它针对 Hotspot JVM 提供了各种特性,以保证基准测试的正确性,整体准确性大大优于其他框架,并且,JMH 还提供了用近乎白盒的方式进行 Profiling 等工作的能力。
为什么需要 JMH 死码消除 所谓死码,是指注释的代码,不可达的代码块,可达但不被使用的代码等等 。
常量折叠与常量传播 常量折叠 (Constant folding) 是一个在编译时期简化常数的一个过程,常数在表示式中仅仅代表一个简单的数值,就像是整数 2
,若是一个变数从未被修改也可作为常数,或者直接将一个变数被明确地被标注为常数,例如下面的描述:
JMH 的注意点
测试前需要预热。
防止无用代码进入测试方法中。
并发测试。
测试结果呈现。
应用场景
当你已经找出了热点函数,而需要对热点函数进行进一步的优化时,就可以使用 JMH 对优化的效果进行定量的分析。
想定量地知道某个函数需要执行多长时间,以及执行时间和输入 n 的相关性
一个函数有两种不同实现(例如 JSON 序列化/反序列化有 Jackson 和 Gson 实现),不知道哪种实现性能更好
JMH 概念
Iteration
- iteration 是 JMH 进行测试的最小单位,包含一组 invocations。
Invocation
- 一次 benchmark 方法调用。
Operation
- benchmark 方法中,被测量操作的执行。如果被测试的操作在 benchmark 方法中循环执行,可以使用@OperationsPerInvocation
表明循环次数,使测试结果为单次 operation 的性能。
Warmup
- 在实际进行 benchmark 前先进行预热。因为某个函数被调用多次之后,JIT 会对其进行编译,通过预热可以使测量结果更加接近真实情况。
JMH 快速入门 添加 maven 依赖 1 2 3 4 5 6 7 8 9 10 11 <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 >
测试代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 import org.openjdk.jmh.annotations.*;import org.openjdk.jmh.runner.*;import java.util.concurrent.TimeUnit;@BenchmarkMode(Mode.Throughput) @Warmup(iterations = 3) @Measurement(iterations = 10, time = 5, timeUnit = TimeUnit.SECONDS) @Threads(8) @Fork(2) @OutputTimeUnit(TimeUnit.MILLISECONDS) public class StringBuilderBenchmark { @Benchmark public void testStringAdd () { String a = "" ; for (int i = 0 ; i < 10 ; i++) { a += i; } } @Benchmark public void testStringBuilderAdd () { StringBuilder sb = new StringBuilder (); for (int i = 0 ; i < 10 ; i++) { sb.append(i); } } public static void main (String[] args) throws RunnerException { Options options = new OptionsBuilder () .include(StringBuilderBenchmark.class.getSimpleName()) .output("d:/Benchmark.log" ) .build(); new Runner (options).run(); } }
执行 JMH 命令行 (1)初始化 benchmarking 工程
1 2 3 4 5 6 7 $ mvn archetype:generate \ -DinteractiveMode=false \ -DarchetypeGroupId=org.openjdk.jmh \ -DarchetypeArtifactId=jmh-java-benchmark-archetype \ -DgroupId=org.sample \ -DartifactId=test \ -Dversion=1.0
(2)构建 benchmark
1 2 cd test/ mvn clean install
(3)运行 benchmark
1 java -jar target/benchmarks.jar
执行 main 方法 执行 main 方法,耐心等待测试结果,最终会生成一个测试报告,内容大致如下;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 # JMH version: 1.22 # VM version: JDK 1.8.0_181, Java HotSpot(TM) 64-Bit Server VM, 25.181-b13 # VM invoker: C:\Program Files\Java\jdk1.8.0_181\jre\bin\java.exe # VM options: -javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2019.2.3\lib\idea_rt.jar=58635:D:\Program Files\JetBrains\IntelliJ IDEA 2019.2.3\bin -Dfile.encoding=UTF-8 # Warmup: 3 iterations, 10 s each # Measurement: 10 iterations, 5 s each # Timeout: 10 min per iteration # Threads: 8 threads, will synchronize iterations # Benchmark mode: Throughput, ops/time # Benchmark: io.github.dunwu.javatech.jmh.StringBuilderBenchmark.testStringAdd # Run progress: 0.00% complete, ETA 00:05:20 # Fork: 1 of 2 # Warmup Iteration 1: 21803.050 ops/ms # Warmup Iteration 2: 22501.860 ops/ms # Warmup Iteration 3: 20953.944 ops/ms Iteration 1: 21627.645 ops/ms Iteration 2: 21215.269 ops/ms Iteration 3: 20863.282 ops/ms Iteration 4: 21617.715 ops/ms Iteration 5: 21695.645 ops/ms Iteration 6: 21886.784 ops/ms Iteration 7: 21986.899 ops/ms Iteration 8: 22389.540 ops/ms Iteration 9: 22507.313 ops/ms Iteration 10: 22124.133 ops/ms # Run progress: 25.00% complete, ETA 00:04:02 # Fork: 2 of 2 # Warmup Iteration 1: 22262.108 ops/ms # Warmup Iteration 2: 21567.804 ops/ms # Warmup Iteration 3: 21787.002 ops/ms Iteration 1: 21598.970 ops/ms Iteration 2: 22486.133 ops/ms Iteration 3: 22157.834 ops/ms Iteration 4: 22321.827 ops/ms Iteration 5: 22477.063 ops/ms Iteration 6: 22154.760 ops/ms Iteration 7: 21561.095 ops/ms Iteration 8: 22194.863 ops/ms Iteration 9: 22493.844 ops/ms Iteration 10: 22568.078 ops/ms Result "io.github.dunwu.javatech.jmh.StringBuilderBenchmark.testStringAdd": 21996.435 ±(99.9%) 412.955 ops/ms [Average] (min, avg, max) = (20863.282, 21996.435, 22568.078), stdev = 475.560 CI (99.9%): [21583.480, 22409.390] (assumes normal distribution) # JMH version: 1.22 # VM version: JDK 1.8.0_181, Java HotSpot(TM) 64-Bit Server VM, 25.181-b13 # VM invoker: C:\Program Files\Java\jdk1.8.0_181\jre\bin\java.exe # VM options: -javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2019.2.3\lib\idea_rt.jar=58635:D:\Program Files\JetBrains\IntelliJ IDEA 2019.2.3\bin -Dfile.encoding=UTF-8 # Warmup: 3 iterations, 10 s each # Measurement: 10 iterations, 5 s each # Timeout: 10 min per iteration # Threads: 8 threads, will synchronize iterations # Benchmark mode: Throughput, ops/time # Benchmark: io.github.dunwu.javatech.jmh.StringBuilderBenchmark.testStringBuilderAdd # Run progress: 50.00% complete, ETA 00:02:41 # Fork: 1 of 2 # Warmup Iteration 1: 241500.886 ops/ms # Warmup Iteration 2: 134206.032 ops/ms # Warmup Iteration 3: 86907.846 ops/ms Iteration 1: 86143.339 ops/ms Iteration 2: 74725.356 ops/ms Iteration 3: 72316.121 ops/ms Iteration 4: 77319.716 ops/ms Iteration 5: 83469.256 ops/ms Iteration 6: 87712.360 ops/ms Iteration 7: 79421.899 ops/ms Iteration 8: 80867.839 ops/ms Iteration 9: 82619.163 ops/ms Iteration 10: 87026.928 ops/ms # Run progress: 75.00% complete, ETA 00:01:20 # Fork: 2 of 2 # Warmup Iteration 1: 228342.337 ops/ms # Warmup Iteration 2: 124737.248 ops/ms # Warmup Iteration 3: 82598.851 ops/ms Iteration 1: 86877.318 ops/ms Iteration 2: 89388.624 ops/ms Iteration 3: 88523.558 ops/ms Iteration 4: 87547.332 ops/ms Iteration 5: 88376.087 ops/ms Iteration 6: 88848.837 ops/ms Iteration 7: 85998.124 ops/ms Iteration 8: 86796.998 ops/ms Iteration 9: 87994.726 ops/ms Iteration 10: 87784.453 ops/ms Result "io.github.dunwu.javatech.jmh.StringBuilderBenchmark.testStringBuilderAdd": 84487.902 ±(99.9%) 4355.525 ops/ms [Average] (min, avg, max) = (72316.121, 84487.902, 89388.624), stdev = 5015.829 CI (99.9%): [80132.377, 88843.427] (assumes normal distribution) # Run complete. Total time: 00:05:23 REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial experiments, perform baseline and negative tests that provide experimental control, make sure the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts. Do not assume the numbers tell you what you want them to tell. Benchmark Mode Cnt Score Error Units StringBuilderBenchmark.testStringAdd thrpt 20 21996.435 ± 412.955 ops/ms StringBuilderBenchmark.testStringBuilderAdd thrpt 20 84487.902 ± 4355.525 ops/ms
JMH API 下面来了解一下 jmh 常用 API
@BenchmarkMode 基准测试类型。这里选择的是 Throughput
也就是吞吐量。根据源码点进去,每种类型后面都有对应的解释,比较好理解,吞吐量会得到单位时间内可以进行的操作数。
Throughput
- 整体吞吐量,例如“1 秒内可以执行多少次调用”。
AverageTime
- 调用的平均时间,例如“每次调用平均耗时 xxx 毫秒”。
SampleTime
- 随机取样,最后输出取样结果的分布,例如“99%的调用在 xxx 毫秒以内,99.99%的调用在 xxx 毫秒以内”
SingleShotTime
- 以上模式都是默认一次 iteration 是 1s,唯有 SingleShotTime 是只运行一次。往往同时把 warmup 次数设为 0,用于测试冷启动时的性能。
All
- 所有模式
@Warmup 上面我们提到了,进行基准测试前需要进行预热。一般我们前几次进行程序测试的时候都会比较慢, 所以要让程序进行几轮预热,保证测试的准确性。其中的参数 iterations 也就非常好理解了,就是预热轮数。
为什么需要预热?因为 JVM 的 JIT 机制的存在,如果某个函数被调用多次之后,JVM 会尝试将其编译成为机器码从而提高执行速度。所以为了让 benchmark 的结果更加接近真实情况就需要进行预热。
@Measurement 度量,其实就是一些基本的测试参数。
iterations
- 进行测试的轮次
time
- 每轮进行的时长
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 主要分为三种。
Thread
- 该状态为每个线程独享。
Group
- 该状态为同一个组里面所有线程共享。
Benchmark
- 该状态在所有线程间共享。
关于 State 的用法,官方的 code sample 里有比较好的例子 。
参考资料