0. 引言
在开发工作中经常会遇到对代码运行速度调优的需要。如何迷信评估调优的成绩?则须要精确计量一个办法运行速度的快慢 — 迷信的做法是进行微基准测试,从而得出量化的后果。JMH 是由 OpenJDK 提供的对 Java 语言程序进行基准测试的工具。本文将介绍的根本用法和一个实用示例。
相干概念
- BenchMark:又叫做基准测试,次要用来测试一些办法的性能,能够依据不同的参数以不同的单位进行计算(例如能够应用吞吐量为单位,也能够应用均匀工夫作为单位,在 BenchmarkMode 外面进行调整)。
- Micro Benchmark:简略地说就是在
method
层面上的 benchmark,精度能够准确到微秒级。 - OPS, Opeartion Per Second: 每秒操作量, 是掂量性能的重要指标,数值越大,性能越好。相似的有 TPS, QPS
- Throughput 吞吐率:
- Warmup 预热:为什么须要预热?因为 JVM 的 JIT 机制的存在,如果某个函数被调用屡次之后,JVM 会尝试将其编译成为机器码从而进步执行速度。程序理论运行中会收到 JVM 的主动优化,为了让 Benchmark 的后果更加靠近真实情况就须要进行预热。
1. 什么是 JMH ?
JMH 全称 Java Microbenchmark Harness,是用于构建、运行和剖析以 Java 和其余基于 JVM 的其余语言编写的 nano/micro/milli/macro 基准测试的 Java 工具。
JMH 官网介绍:“JMH is a Java harness for building, running, and analysing nano/micro/milli/macro benchmarks written in Java and other languages targetting the JVM.”
2. JMH 能做什么?
性能介绍
一句话概括 JMH 的作用是 度量(measure)某个办法的执行耗时,能够通过执行 JMH 测试得出办法执行耗时的量化后果。
应用场景
JMH 适用范围示例:
- a. 度量某个办法执行耗时
- b. 度量某个办法执行工夫和输出 n 的相关性
- c. 评估一个办法的多种不同实现性能体现
- d. 评估利用中调用的第三方库 API 的执行性能
- e. b&c 综合利用
JMH 理论利用示例:
- 评估 ArrayList 遍历性能与输出 n 的相关性
- 比拟 ArrayList 和 LinkedList 遍历性能与输出 n 的相关性,并比拟差别
- 评估 redis-client Java 库 put 办法的性能
- 比拟实现求和的两种办法在 N 次输出下的性能差别,办法 methodSumA 应用了 Stream API,办法 methodSumB 应用了传统遍历累加,须要测试两种办法在不同数据量输出时的体现性能线性变动。通过 JMH 测试,能够对不同量级数据输出时如何抉择合适的求和实现起到指导作用。
3. 如何应用
以比拟 for 循环实现求和与 Stream API 实现求和的办法,在输出数据量级分为别为 10000, 100000, 1000000, 10000000 时求和的性能为例,介绍如何应用 JMH 实现这一测试。
测试程序执行过程形容伪代码:
forLoopMethod() {loop (size in (10000, 100000, 1000000, 10000000)) {// 遍历不同输出数量
doSumFromZeroTo(size);// 以 for 循环形式累加 0~size 求和
}
}
streamMethod() {loop (size in (10000, 100000, 1000000, 10000000)) {// 遍历不同输出数量
doSumFromZeroTo(size);// 以 stream sum API 形式累加 0~size 求和
}
}
3.1. 创立工程
创立工程
以 Maven 构建的工程为例
应用的 JDK 版本为 1.8
增加以下 dependency 节点向工程中引入依赖
<properties>
<!-- 尽量抉择最新版本 -->
<jmh.version>1.28</jmh.version>
</properties>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<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>
</dependencies>
3.2. 参数阐明
运行 JMH 前须进行肯定的设置,JMH 的设置项参数能够通过
new Runner(org.openjdk.jmh.runner.options.Options)
形式注入。JMH 实现了 JSR269 标准,即注解处理器,能在编译 Java 源码的时候,辨认的到须要解决的注解,如 @Beanmark,JMH 能依据 @Beanmark 的配置生成一系列测试辅助类。因而也能够通过注解形式注入设置项参数。
以下以注解为例介绍每个设置项参数的作用。
-
@BenchmarkMode
Mode 示意 JMH 进行 Benchmark 时所应用的模式。通常是测量的维度不同,或是测量的形式不同。目前 JMH 共有四种模式:
- Throughput: 整体吞吐量,例如“1 秒内能够执行多少次调用”,单位是操作数 / 工夫。
- AverageTime: 调用的均匀工夫,例如“每次调用均匀耗时 xxx 毫秒”,单位是工夫 / 操作数。
- SampleTime: 随机取样,最初输入取样后果的散布,例如“99% 的调用在 xxx 毫秒以内,99.99% 的调用在 xxx 毫秒以内”。
- SingleShotTime: 以上模式都是默认一次 iteration 是 1s,唯有 SingleShotTime 是只运行一次。往往同时把 warmup 次数设为 0,用于测试冷启动时的性能。
-
@OutputTimeUnit
输入的工夫单位。
-
@Iteration
Iteration 是 JMH 进行测试的最小单位。在大部分模式下,一次 iteration 代表的是一秒,JMH 会在这一秒内一直调用须要 Benchmark 的办法,而后依据模式对其采样,计算吞吐量,计算均匀执行工夫等。
-
@WarmUp
Warmup 是指在理论进行 Benchmark 前先进行预热的行为。
为什么须要预热?因为 JVM 的 JIT 机制的存在,如果某个函数被调用屡次之后,JVM 会尝试将其编译成为机器码从而进步执行速度。为了让 Benchmark 的后果更加靠近真实情况就须要进行预热。
-
@State
类注解,JMH 测试类必须应用 @State 注解,它定义了一个类实例的生命周期,能够类比 Spring Bean 的 Scope。因为 JMH 容许多线程同时执行测试,不同的选项含意如下:
- Scope.Thread:默认的 State,每个测试线程调配一个实例;
- Scope.Benchmark:所有测试线程共享一个实例,用于测试有状态实例在多线程共享下的性能;
- Scope.Group:每个线程组共享一个实例;
-
@Fork
进行 fork 的次数。如果 fork 数是 2 的话,则 JMH 会 fork 出两个过程来进行测试。
-
@Meansurement
提供真正的测试阶段参数。指定迭代的次数,每次迭代的运行工夫和每次迭代测试调用的数量(通常应用 @BenchmarkMode(Mode.SingleShotTime) 测试一组操作的开销——而不应用循环)
-
@Setup
办法注解,会在执行 benchmark 之前被执行,正如其名,次要用于初始化。
-
@TearDown
办法注解,与 @Setup 绝对的,会在所有 benchmark 执行完结当前执行,次要用于资源的回收等。
@Setup/@TearDown 注解应用 Level 参数来指定何时调用 fixture:
名称 形容 Level.Trial 默认 level。全副 benchmark 运行 (一组迭代) 之前 / 之后 Level.Iteration 一次迭代之前 / 之后(一组调用) Level.Invocation 每个办法调用之前 / 之后(不举荐应用,除非你分明这样做的目标) -
@Benchmark
办法注解,示意该办法是须要进行 benchmark 的对象。
-
@Param
成员注解,能够用来指定某项参数的多种状况。特地适宜用来测试一个函数在不同的参数输出的状况下的性能。@Param 注解接管一个 String 数组,在 @Setup 办法执行前转化为为对应的数据类型。多个 @Param 注解的成员之间是乘积关系,譬如有两个用 @Param 注解的字段,第一个有 5 个值,第二个字段有 2 个值,那么每个测试方法会跑 5 *2=10 次。
3.3. 编写测试类
蕴含 main 办法的类和测试内容封装类
- 蕴含 main 办法的类
TestsMain
package org.example.jmh;
import org.example.jmh.tests.IntegerSumTests;
import org.openjdk.jmh.annotations.Mode;
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.ChainedOptionsBuilder;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertTrue;
/**
* OptionsTests
* created at 2021/4/25
*
* @author weny
* @since 1.0.0
*/
public class TestsMain {// 生成的文件门路:{工程根目录}/{reportFileDir}/{XXX.class.getSimpleName()}.json
// e.g. jmh-reports/EmptyMethod.json
private static final String reportFileDir = "jmh-reports/";
// private static final String reportPath = "sample-options-result.json";// 生成的文件在工程根目录
/*
* ============================== HOW TO RUN THIS TEST: ====================================
* 1. 批改 Class<IntegerSumTests> targetClazz = IntegerSumTests.class;// 须要运行 JMH 测试的类
* 2. 在 IDE 中运行 main 办法
*/
public static void main(String[] args) throws RunnerException {
Class<IntegerSumTests> targetClazz = IntegerSumTests.class;// 须要运行 JMH 测试的类
String reportFilePath = setupStandardOptions(targetClazz);
assertTrue(Files.exists(Paths.get(reportFilePath)));
}
/**
* 最根底的配置,目标是以最短的耗时测试 JMH 是否能够失常运行
*
* @param targetClazz 要运行 JMH 测试的类
* @throws RunnerException See:{@link RunnerException}
*/
@SuppressWarnings({"unused"})
private static String setupBasicOptions(Class<?> targetClazz) throws RunnerException {
// number of iterations is kept to a minimum just to verify that the benchmarks work without spending extra
// time during builds.
String reportFilePath = resolvePath(targetClazz);
ChainedOptionsBuilder optionsBuilder =
new OptionsBuilder()
.include(targetClazz.getSimpleName())
.forks(1)
.warmupIterations(0)
.measurementBatchSize(1)
.measurementIterations(1)
.shouldFailOnError(true)
.result(reportFilePath)
.timeUnit(TimeUnit.MICROSECONDS)
.resultFormat(ResultFormatType.JSON);
new Runner(optionsBuilder.build()).run();
return reportFilePath;
}
/**
* 一份规范的配置,依据理论需要配置预热和迭代等参数
*
* @param targetClazz 要运行 JMH 测试的类
* @throws RunnerException See:{@link RunnerException}
*/
private static String setupStandardOptions(Class<?> targetClazz) throws RunnerException {String reportFilePath = resolvePath(targetClazz);
ChainedOptionsBuilder optionsBuilder =
new OptionsBuilder()
.include(targetClazz.getSimpleName())
.mode(Mode.Throughput)// 模式 - 吞吐量 | 注解形式 @BenchmarkMode(Mode.Throughput)
.forks(1)//Fork 进行的数目 | 注解形式 @Fork(2)
.warmupIterations(1)// 预热轮数 | 注解形式 @Warmup(iterations = 1)
.measurementIterations(3)// 度量轮数 | 注解形式 @Measurement(iterations = 3)
.timeUnit(TimeUnit.MICROSECONDS)// 后果所应用的工夫单位 | 注解形式 @OutputTimeUnit(TimeUnit.MILLISECONDS)
.shouldFailOnError(true)
.result(reportFilePath)// 后果报告文件输入门路
.resultFormat(ResultFormatType.JSON);// 后果报告文件输入格局 JSON
new Runner(optionsBuilder.build()).run();
return reportFilePath;
}
private static String resolvePath(Class<?> targetClazz) {return reportFileDir + targetClazz.getSimpleName() + ".json";
}
}
- 测试内容封装类
EmptyMethod
蕴含空的办法,用于测试配置是否能够失常运行
package org.example.jmh.tests;
import org.openjdk.jmh.annotations.Benchmark;
/**
* EmptyMethod
* created at 2021/4/25
*
* @author weny
* @since 1.0.0
*/
public class EmptyMethod {
@Benchmark
public void hello() {// this method was intentionally left blank.}
}
- 测试内容封装类
IntegerSumTests
蕴含待度量评估的办法streamSummingInt
,forEachPlus
package org.example.jmh.tests;
import org.openjdk.jmh.annotations.*;
import java.util.Arrays;
import java.util.stream.IntStream;
/**
* IntegerSumTests
* created at 2021/4/25
*
* @author weny
* @since 1.0.0
*/
public class IntegerSumTests {
// Implementation using stream summingInt
@Benchmark
public int streamSummingInt(Params params) {return Arrays.stream(params.items).sum();}
// Implementation using forEach
@Benchmark
public int forEachPlus(Params params) {
int res = 0;
for (int item : params.items) {res += item;}
return res;
}
// Define benchmarks parameters with @State
@State(Scope.Benchmark)
public static class Params {
// Run with given size parameters of
// @Param({"1000", "10000", "100000", "1000000"})
@Param({"10000", "100000", "1000000", "10000000"})
public int size;
// Items to run benchmark on
public int[] items;
// Setup test data, will be run once and will not affect our results
@Setup
public void setUp() {items = IntStream.range(0, size).toArray();}
}
}
3.4. 运行测试
运行测试得出后果数据
运行 Main 办法 org.example.jmh.TestsMain#main
3.5. 后果剖析
对测试后果数据项进行剖析
运行开始 – 参数打印
# JMH version: 1.28
# VM version: JDK 1.8.0_162, Java HotSpot(TM) 64-Bit Server VM, 25.162-b12
# VM invoker: /Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/bin/java
# VM options: -javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=58962:/Applications/IntelliJ IDEA.app/Contents/bin -Dfile.encoding=UTF-8
# Blackhole mode: full + dont-inline hint
# Warmup: 1 iterations, 10 s each ------------------------------ 预热 5 个迭代,每个迭代 10s
# Measurement: 3 iterations, 10 s each --------------------------- 正式测试 5 个迭代,每个迭代 10s
# Timeout: 10 min per iteration ---------------------------------- 每个迭代的超时工夫 10min
# Threads: 1 thread, will synchronize iterations ----------------- 应用 1 个线程测试
# Benchmark mode: Throughput, ops/time --------------------------- 应用吞吐量作为测试指标
# Benchmark: org.example.jmh.tests.IntegerSumTests.forEachPlus --- 本次迭代测试的指标办法名
# Parameters: (size = 10000) ------------------------------------- 本次迭代注入的参数值
运行中 – 阶段信息打印
# Run progress: 12.50% complete, ETA 00:04:47 ---------------------- 运行进度 12.50%
# Fork: 1 of 1
# Warmup Iteration 1: 32206.476 ops/s
Iteration 1: 32631.226 ops/s
Iteration 2: 32725.618 ops/s
Iteration 3: 32681.244 ops/s
Result "org.example.jmh.tests.IntegerSumTests.forEachPlus": -------- 阶段后果统计
32679.362 ±(99.9%) 861.539 ops/s [Average]
(min, avg, max) = (32631.226, 32679.362, 32725.618), stdev = 47.224
CI (99.9%): [31817.824, 33540.901] (assumes normal distribution)
# 统计后果给出了屡次测试后的最小值,最大值和均值,以及标准差 (stdev), 置信区间(CI,Confidence interval)
# 标准差(stdev)反映了数值绝对于均匀值得离散水平,置信区间是指由样本统计量所结构的总体参数的预计区间。在统计学中,一个概率样本的置信区间(Confidence interval)是对这个样本的某个总体参数的区间预计
运行完结 – 后果打印
# Run complete. Total time: 00:05:26
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 (size) Mode Cnt Score Error Units
IntegerSumTests.forEachPlus 10000 thrpt 3 328440.729 ± 92008.121 ops/s
IntegerSumTests.forEachPlus 100000 thrpt 3 32679.362 ± 861.539 ops/s
IntegerSumTests.forEachPlus 1000000 thrpt 3 2796.351 ± 3273.468 ops/s
IntegerSumTests.forEachPlus 10000000 thrpt 3 218.404 ± 36.500 ops/s
IntegerSumTests.streamSummingInt 10000 thrpt 3 50423.644 ± 47232.940 ops/s
IntegerSumTests.streamSummingInt 100000 thrpt 3 7521.114 ± 47042.316 ops/s
IntegerSumTests.streamSummingInt 1000000 thrpt 3 480.979 ± 112.349 ops/s
IntegerSumTests.streamSummingInt 10000000 thrpt 3 132.339 ± 1514.091 ops/s
Benchmark result is saved to jmh-reports/IntegerSumTests.json
Process finished with exit code 0
# Benchmark 列示意这次测试比照的办法。# Mode 列表上后果的统计纬度。# Cnt 列示意采样次数,Cnt=Fork*Iteration.
# Score 是对这次评测的打分,对于输出数据量 size=10000 时 forEachPlus 操作数为 328440.729 ops/s,streamSummingInt 时 操作数为 50423.644 ops/s, 意味着 size=10000 时 for 循环求和性能优于 stream.sum() 求和。# Error 这里示意性能统计上的误差,咱们不须要关怀这个数据,次要查看 Score
3.6. 后果报告可视化
将后果生成可视化的报表
可选形式:
一、将后果 json 文件上传至 JMH Visualizer 主动生成报表,参考:JMH Visualizer
二、将后果通过第三方报表组件自定义出现,此处不作开展。
4. 注意事项
4.1. 须要思考到虚拟机的优化
编写 JHM 代码,须要思考到虚拟机的优化,而使得测试失真,如下 measureWrong 代码就是所谓的 Dead-Code
代码
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class JMHSample_08_DeadCode {
private double x = Math.PI;
@Benchmark
public void baseline() {// 基准}
@Benchmark
public void measureWrong() {
// 虚构机会优化掉这部分,性能同 baseline
Math.log(x);
}
@Benchmark
public double measureRight() {
// 真正的性能测试
return Math.log(x);
}
}
测试后果如下
Benchmark Mode Score Units
c.i.c.c.c.i.c.c.j.JMHSample_08_DeadCode.baseline avgt 0.358 ns/op
c.i.c.c.c.i.c.c.j.JMHSample_08_DeadCode.measureRight avgt 24.605 ns/op
c.i.c.c.c.i.c.c.j.JMHSample_08_DeadCode.measureWrong avgt 0.366 ns/op
在测试 measureWrong 办法,JIT 能揣测出办法体能够被优化调而不影响零碎,measureRight 因为定义了返回值,JIT 不会优化。
4.2. 常量折叠
对于常量折叠,JIT 认为办法计算结果为常量,从而优化间接返回常量给调用者
private double x = Math.PI;
private final double wrongX = Math.PI;
@Benchmark
public double baseline() {
// 基准测试
return Math.PI;
}
@Benchmark
public double measureWrong_1() {
// JIT 认为是个常量
return Math.log(Math.PI);
}
@Benchmark
public double measureWrong_2() {
// JIT 认为办法调用后果是个常量.
return Math.log(wrongX);
}
@Benchmark
public double measureRight() {
// 正确的测试
return Math.log(x);
}
如下是测试后果
Benchmark Mode Score Units
c.i.c.c.c.i.c.c.j.JMHSample_10_ConstantFold.baseline avgt 1.175 ns/op
c.i.c.c.c.i.c.c.j.JMHSample_10_ConstantFold.measureRight avgt 25.805 ns/op
c.i.c.c.c.i.c.c.j.JMHSample_10_ConstantFold.measureWrong_1 avgt 1.116 ns/op
c.i.c.c.c.i.c.c.j.JMHSample_10_ConstantFold.measureWrong_2 avgt 1.031 ns/op
思考到 inline 对性能影响很大,JMH 反对 @CompilerControl 来管制是否容许内联
public class Inline {
int x=0,y=0;
@Benchmark
@CompilerControl(CompilerControl.Mode.DONT_INLINE)
public int add(){return dataAdd(x,y);
}
@Benchmark
public int addInline(){return dataAdd(x,y);
}
private int dataAdd(int x,int y){return x+y;}
@Setup
public void init() {
x = 1;
y = 2;
}
}
add 和 addInline 办法都会调用 dataAdd 办法,前者应用 CompilerControl 类,能够用在办法或者类上,来提供编译选项
- DONT_INLINE,调用办法不内联
- INLINE,调用办法内联
- BREAK,插入一个调试断点(TODO, 如何调试,参考 11 章)
- PRINT,打印办法被 JIT 编译后的机器码信息
开发人员可能感觉下面的测试,add 办法太简略,会习惯性的在 add 办法里方一个循环,以缩小 JMH 调用 add 办法的老本。JMH 不倡议这么做,因为 JIT 会实际上对这种循环会做优化,以打消循环调用老本。如下是个例子能够看到循环测试后果不精确
int x = 1;
int y = 2;
/** 正确测试
*/
@Benchmark
public int measureRight() {return (x + y);
}
private int reps(int reps) {
int s = 0;
for (int i = 0; i < reps; i++) {s += (x + y);
}
return s;
}
@Benchmark
@OperationsPerInvocation(1)
public int measureWrong_1() {return reps(1);
}
@Benchmark
@OperationsPerInvocation(10)
public int measureWrong_10() {return reps(10);
}
@Benchmark
@OperationsPerInvocation(100)
public int measureWrong_100() {return reps(100);
}
@Benchmark
@OperationsPerInvocation(1000)
public int measureWrong_1000() {return reps(1000);
}
注解 OperationsPerInvocation 通知 JMH 统计性能的时候须要做修改,比方 @OperationsPerInvocation(10) 调用了 10 次。
性能测试后果如下
编写性能测试的一个好习惯是先编写一个单元测试用例,以确保性能测试准确性,x Benchmark Mode Score Units c.i.c.c.c.i.c.c.j.JMHSample_11_Loops.measureRight avgt 1.114 ns/op c.i.c.c.c.i.c.c.j.JMHSample_11_oops.measureWrong_1 avgt 1.057 ns/op c.i.c.c.c.i.c.c.j.JMHSample_11_Loops.measureWrong_10 avgt 0.139 ns/op c.i.c.c.c.i.c.c.j.JMHSample_11_Loops.measureWrong_100 avgt 0.018 ns/op c.i.c.c.c.i.c.c.j.JMHSample_11_Loops.measureWrong_1000 avgt 0.035 ns/op java
5. 结语
通过 JMH 测试,能够和迷信的评估办法的执行耗时,评估后果能够对性能调优、算法性能预测、服务器基础设施容量布局等行为起到指导作用。量化的后果更有说服力,对后果进一步可视化,将更加直观。
6. 扩大浏览
- 官网的 Code Sample 写得浅显易懂,举荐在须要具体理解 JMH 的用法时能够通读一遍。
- 如果应用 IDEA,IntelliJ IDEA 有 JMH 的插件,提供 benchmark 办法的主动生成等便当性能。
- RPC benchmark 实现:java rpc benchmark https://github.com/hank-whu/r…
7. 参考
Reference List:
- JMH Github Repo
- OpenJDK JMH Samples
- JMH Samples 中文版
- 应用 JMH 做 Benchmark 基准测试
- Java 并发编程笔记:JMH 性能测试框架
- Java 性能优化 - 把握 JMH
- JMH – Java Microbenchmark Harness
- JMH Visualizer
- jmh-visual-chart