前言
先提出一个问题,大家认为高铁快还是动车快?有人说高铁快,有人说动车快,为什么高铁快呢?因为技术上的指标看高铁是快的,但是为什么又有人说动车快呢?因为有时候会在12306上发现动车的车次比高铁的车次运行时间还长。那么到底谁快谁慢呢?要想回到这个问题,个人觉的要把比较的标准进行统一才能回答,如果以理论实验室速度来进行比较,显然高铁是快的。
同样在实际的开发中,也经常会遇到谁快谁慢的问题,比如 foreach快还是stream.foreach亦或是fori快呢?各种序列化方法(protobuf、kory、hession)哪种更快呢?要想回答上面的问题,或者出自何种原因需要进行性能评估,量化指标同样是十分必要的。
在大部分场合,简单地回答谁快谁慢是远远不够的,通过我们需要提供具体的详细数据来阐述谁快谁慢,那么,如何将程序性能量化呢?本文通过Java Microbenchmark Harness (JMH)
来衡量,对性能指标进行量化。
什么是JMH
Java Microbenchmark Harness (JMH)
是一个 Java 工具,用于构建、运行和分析以 Java 和其他针对 JVM 的语言编写的 nano/micro/milli/macro 基准。
JMH可以做什么
JMH 为openjdk官方提供的基准测量工具,当通过类似于arthas等工具定位到具体热点方法后,通过分析调优,希望进一步优化方法性能的时候,就可以使用 JMH 对优化的结果进行量化的分析。
JMH 比较典型的应用场景如下:
- 想准确地知道某个方法需要执行多长时间,以及执行时间和输入之间的相关性
- 对比接口不同实现在给定条件下的吞吐量
- 查看多少百分比的请求在多长时间内完成
JMH引入
在JDK9
中JMH
已被默认引入,但如果是JDK9
之前的版本需要加入如下依赖(截止2022-09-23JMH
的最新版本为1.35
),如果需要获取最新版本数据,可以访问github查看:openjdk/jmh。
<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>
JMH的使用
实际案例
在介绍实际的使用语法前,首先来看个案例。
import com.netflix.hollow.core.memory.ByteArrayOrdinalMap;
import com.netflix.hollow.core.memory.ByteDataArray;
import java.util.SplittableRandom;
import java.util.concurrent.TimeUnit;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 1)
@Fork(1)
public class OrdinalMapResize {
@Param("256")
int n = 256;
@Param("32")
int contentSize = 32;
ByteDataArray[] content;
@Setup
public void setUp() {
SplittableRandom r = new SplittableRandom(0);
content = new ByteDataArray[n];
for (int i = 0; i < n; i++) {
ByteDataArray buf = new ByteDataArray();
for (int j = 0; j < contentSize; j++) {
buf.write((byte) r.nextInt(0, 256));
}
content[i] = buf;
}
}
@Benchmark
public ByteArrayOrdinalMap defaultGet() {
ByteArrayOrdinalMap map = new ByteArrayOrdinalMap();
for (int i = 0; i < n; i++) {
map.getOrAssignOrdinal(content[i]);
}
return map;
}
@Benchmark
public ByteArrayOrdinalMap sizedGet() {
ByteArrayOrdinalMap map = new ByteArrayOrdinalMap(n << 1);
for (int i = 0; i < n; i++) {
map.getOrAssignOrdinal(content[i]);
}
return map;
}
}
运行结果:
Benchmark (contentSize) (n) Mode Cnt Score Error Units
OrdinalMapResize.defaultGet 8 2048 avgt 5 524669.559 ± 94661.993 ns/op
OrdinalMapResize.defaultGet 8 8192 avgt 5 2864984.097 ± 973573.341 ns/op
OrdinalMapResize.defaultGet 8 32768 avgt 5 14644216.493 ± 461996.760 ns/op
OrdinalMapResize.defaultGet 8 131072 avgt 5 64906816.288 ± 5527376.480 ns/op
OrdinalMapResize.defaultGet 16 2048 avgt 5 579804.853 ± 35227.500 ns/op
OrdinalMapResize.defaultGet 16 8192 avgt 5 3226875.857 ± 1200816.411 ns/op
OrdinalMapResize.defaultGet 16 32768 avgt 5 16333522.740 ± 1645155.319 ns/op
OrdinalMapResize.defaultGet 16 131072 avgt 5 72300430.931 ± 8243987.662 ns/op
OrdinalMapResize.defaultGet 32 2048 avgt 5 708348.638 ± 48480.615 ns/op
OrdinalMapResize.defaultGet 32 8192 avgt 5 3845712.806 ± 659749.886 ns/op
OrdinalMapResize.defaultGet 32 32768 avgt 5 19132132.938 ± 2578367.604 ns/op
OrdinalMapResize.defaultGet 32 131072 avgt 5 81810115.046 ± 5377543.308 ns/op
OrdinalMapResize.defaultGet 64 2048 avgt 5 988812.959 ± 139916.394 ns/op
OrdinalMapResize.defaultGet 64 8192 avgt 5 5247170.001 ± 961661.753 ns/op
OrdinalMapResize.defaultGet 64 32768 avgt 5 24915997.247 ± 5133110.667 ns/op
OrdinalMapResize.defaultGet 64 131072 avgt 5 106995398.542 ± 14459051.669 ns/op
OrdinalMapResize.defaultGet 128 2048 avgt 5 1575062.220 ± 110460.192 ns/op
OrdinalMapResize.defaultGet 128 8192 avgt 5 7735092.080 ± 411007.955 ns/op
OrdinalMapResize.defaultGet 128 32768 avgt 5 35920602.082 ± 7833800.705 ns/op
OrdinalMapResize.defaultGet 128 131072 avgt 5 156354984.171 ± 13402008.695 ns/op
OrdinalMapResize.sizedGet 8 2048 avgt 5 196358.861 ± 15371.176 ns/op
OrdinalMapResize.sizedGet 8 8192 avgt 5 1250185.898 ± 337449.745 ns/op
OrdinalMapResize.sizedGet 8 32768 avgt 5 7569535.480 ± 768681.941 ns/op
OrdinalMapResize.sizedGet 8 131072 avgt 5 34166531.026 ± 2601826.357 ns/op
OrdinalMapResize.sizedGet 16 2048 avgt 5 233474.915 ± 4107.039 ns/op
OrdinalMapResize.sizedGet 16 8192 avgt 5 1618315.403 ± 503515.257 ns/op
OrdinalMapResize.sizedGet 16 32768 avgt 5 8423144.202 ± 1531274.146 ns/op
OrdinalMapResize.sizedGet 16 131072 avgt 5 37380396.196 ± 3756140.945 ns/op
OrdinalMapResize.sizedGet 32 2048 avgt 5 291975.497 ± 30660.463 ns/op
OrdinalMapResize.sizedGet 32 8192 avgt 5 1930925.813 ± 162060.695 ns/op
OrdinalMapResize.sizedGet 32 32768 avgt 5 9944128.175 ± 2468853.190 ns/op
OrdinalMapResize.sizedGet 32 131072 avgt 5 43925098.941 ± 3336023.251 ns/op
OrdinalMapResize.sizedGet 64 2048 avgt 5 423357.848 ± 60510.201 ns/op
OrdinalMapResize.sizedGet 64 8192 avgt 5 2553086.582 ± 298923.441 ns/op
OrdinalMapResize.sizedGet 64 32768 avgt 5 12388266.041 ± 4230163.664 ns/op
OrdinalMapResize.sizedGet 64 131072 avgt 5 55145939.840 ± 6016040.570 ns/op
OrdinalMapResize.sizedGet 128 2048 avgt 5 687408.861 ± 39354.473 ns/op
OrdinalMapResize.sizedGet 128 8192 avgt 5 4023545.068 ± 124931.913 ns/op
OrdinalMapResize.sizedGet 128 32768 avgt 5 17758789.247 ± 5952902.676 ns/op
OrdinalMapResize.sizedGet 128 131072 avgt 5 77575714.649 ± 10985599.044 ns/op
以上是在hollow工程中的一个jmh测试的案例。
同样,官方也提供了几十个sample以供参考,地址:jmh-samples
使用jar包执行基准测试
对于一些小测试,直接用上文类似于jmh-samples写一个 main 函数手动执行就好了。
对于大型的测试,需要测试的时间比较久、线程数比较多,加上测试的服务器需要,一般要放在服务器里去执行。
运行 JMH
基准测试的推荐方法是使用 Maven
设置依赖于应用程序的 jar
文件的独立项目。
这种方法是首选,以确保正确初始化基准并产生可靠的结果。 同样,可以在现有项目中运行基准测试,甚至可以从 IDE
中运行,但是设置更复杂,结果也不太可靠。
在所有情况下,使用 JMH
的关键是启用注释或字节码处理器来生成综合基准代码。Maven
原型是用于引导具有正确构建配置的项目的主要机制。 JMH官方强烈建议新用户使用原型来设置正确的环境。
JMH 官方提供了生成 jar 包的方式来执行,我们需要在 maven 里增加一个 plugin,具体配置如下:
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.4.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<finalName>jmh-demo</finalName>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>org.openjdk.jmh.Main</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
如果使用的是gradle构建项目工程,改如何做呢?可以按照如下方法:
plugins {
id "me.champeau.gradle.jmh" version "0.5.3"
}
apply plugin: 'java'
apply plugin: 'me.champeau.gradle.jmh'
dependencies {
implementation project(':hollow')
implementation 'org.openjdk.jmh:jmh-core:1.21'
compileOnly 'org.openjdk.jmh:jmh-generator-annprocess:1.21'
}
compileJava {
options.compilerArgs << '-XDignore.symbol.file'
options.fork = true
options.forkOptions.executable = 'javac'
}
jmh {
duplicateClassesStrategy = 'warn'
}
命令行执行
创建基准测试项目
以下命令将在 DartifactId
文件夹中生成新的 JMH
驱动项目:
$ mvn archetype:generate \
-DinteractiveMode=false \
-DarchetypeGroupId=org.openjdk.jmh \
-DarchetypeArtifactId=jmh-java-benchmark-archetype \
-DgroupId=org.sample \
-DartifactId=test \
-Dversion=1.0
JMH 支持任意运行可以运行在JVM的编程语言,通过修改DarchetypeArtifactId
参数可以调整不同的语言实现。支持的开发语言清单:artifactId list 。除Java外,还支持groovy、kotlin、scala等语言,具体artifactId如下:
- jmh-java-benchmark-archetype
- jmh-groovy-benchmark-archetype
- jmh-kotlin-benchmark-archetype
- jmh-scala-benchmark-archetype
建立基准测试
项目生成后,可以使用以下 Maven 命令构建它:
$ cd test/
$ mvn clean verify
运行基准测试
构建完成后,将获得独立的可执行jar,其中包含基准测试和所有基本的 JMH 基础架构代码:
$ java -jar target/benchmarks.jar
jmh的指令支持一些参数,如下:
- 打印帮助信息
java -jar hollow-perf/build/libs/hollow-perf-*-jmh.jar -h
- 执行基准测试
java -jar hollow-perf/build/libs/hollow-perf-*-jmh.jar -l
在处理大型项目时,通常将基准测试保存在单独的子项目中,然后通过通常的构建依赖项依赖于测试模块。
JMH基本语法
JMH有提供了20中不同的注解,每种注解都有各自的功能。全部源码地址:jmh-annotations,本小节选择其中的的8中稍微介绍下,
@Benchmark
基准测试注解,在需要测试的方法上添加即可。
@Benchmark
public int checkSum() {
return typeReadState.getChecksum(schema).intValue();
}
@BenchmarkMode
用来配置 Mode
选项,可用于类或者方法上.
这个注解的 value
是一个数组,可以把几种 Mode 集合在一起执行,如:@BenchmarkMode({Mode.SampleTime, Mode.AverageTime})
,
还可以设置为 Mode.All
,即全部执行一遍。
Throughput
:整体吞吐量,每秒执行了多少次调用,单位为ops/time
AverageTime
:用的平均时间,每次操作的平均时间,单位为time/op
SampleTime
:随机取样,最后输出取样结果的分布SingleShotTime
:只运行一次,往往同时把Warmup
次数设为0
,用于测试冷启动时的性能All
:上面的所有模式都执行一次
这里有一段源码,这部分源码中的注解也会在后文中有所体现。
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 1)
@Fork(1)
public class CheckSumCollections {
@Param
指定某项参数的多种情况,特别适合用来测试一个函数在不同的参数输入的情况下的性能,只能作用在字段上,使用该注解必须定义 @State
注解。
@Param("List")
private Type type = Type.Map;
@Param("100")
private int n = 1000;
@Param("100")
private int size = 1000;
@Param("8")
private int shards = 8;
@Param("false")
private boolean remove = false;
@Setup
@Setup
注解可以在进行基准测试前生成一些运行的数据。
@Setup
public void setUp() {
SplittableRandom r = new SplittableRandom(0);
content = new ByteDataArray[n];
for (int i = 0; i < n; i++) {
ByteDataArray buf = new ByteDataArray();
for (int j = 0; j < contentSize; j++) {
buf.write((byte) r.nextInt(0, 256));
}
content[i] = buf;
}
}
@State
通过 State
可以指定一个对象的作用范围,JMH
根据 scope
来进行实例化和共享操作。
@State
可以被继承使用,如果父类定义了该注解,子类则无需定义。
由于 JMH
允许多线程同时执行测试,不同的选项含义如下:
Scope.Benchmark
:所有测试线程共享一个实例,测试有状态实例在多线程共享下的性能Scope.Group
:同一个线程在同一个group
里共享实例Scope.Thread
:默认的State
,每个测试线程分配一个实例
@OutputTimeUnit
为统计结果的时间单位
,可用于类或者方法注解。
@Warmup
预热
所需要配置的一些基本测试参数,可用于类或者方法上。一般前几次进行程序测试的时候都会比较慢,所以要让程序进行几轮预热,保证测试的准确性。参数如下所示:
iterations
:预热的次数time
:每次预热的时间timeUnit
:时间的单位,默认秒batchSize
:批处理大小,每次操作调用几次方法
- 为什么需要预热?
- 因为
JVM
的 JIT 机制的存在,如果某个函数被调用多次之后,JVM 会尝试将其编译为机器码,从而提高执行速度,所以为了让benchmark
的结果更加接近真实情况就需要进行预热。
- 因为
@Measurement
实际调用方法所需要配置的一些基本测试参数,可用于类或者方法上,参数和 @Warmup
相同。
@Threads
每个进程中的测试线程,可用于类或者方法上。
@Fork
进行 fork
的次数,可用于类或者方法上。
举例来讲:如果 fork 数是 2 的话,则 JMH 会 fork 出两个进程来进行测试。
JMH Plugin
openjdk官方提供了jmh的基础能力,但是很多时候并不完善或者官方并不计划提供,这时候社区的大牛会开发很多的扩展插件,本小节将介绍这部分的内容。
Gradle JMH Plugin
JMH
官方不为Maven以外的构建系统提供构建脚本。那么对于其他构建工具的项目如何使用呢?这部分的功能有很多社区的大牛创建了不同构建工具的项目,这里我们着重说下Gradle的构建。
引入依赖
在build.gradle
的添加jmh的插件。
plugins {
id "me.champeau.jmh" version "0.6.8"
}
新建jmh目录
由于特定的配置,该插件可以轻松集成到现有项目中。特别是,基准源文件预计可以在 src/jmh 目录中找到:
src/jmh
|- java : java sources for benchmarks
|- resources : resources for benchmarks
依赖升级
最新的me.champeau.jmh
插件使用 JMH 1.35
。也可以通过更改依赖项块中的版本来升级版本:
dependencies {
jmh 'org.openjdk.jmh:jmh-core:0.9'
jmh 'org.openjdk.jmh:jmh-generator-annprocess:0.9'
}
一个案例
这里列举一个完整的build.gradle
文件。
plugins {
id 'java-library'
id "me.champeau.jmh" version "0.6.7"
}
repositories {
mavenCentral()
}
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.2'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
}
tasks.named('test') {
useJUnitPlatform()
}
jmh {
warmupIterations = 2
iterations = 5
fork = 1
}
Intellij IDEA JMH Plugin
Intellij IDEA JMH Plugin
插件,允许您以与 JUnit
相同的方式使用 JMH
。官网地址:github.com/artyushov/i…
以下是已经实现的功能:
@Benchmark
方法生成- 运行单独的
@Benchmark
方法 - 在一个类中运行所有基准测试
JMH可视化
如果你想将测试结果以图表的形式可视化,可以试下这些网站:
- JMH Visual Chart:deepoove.com/jmh-visual-…
- JMH Visualizer:jmh.morethan.io/
进入可视化页面,上传JMH的JSON格式结果文件result.json(下载result.json示例文件)进行图表渲染:
支持切换横向视图和垂直视图。
总结
Java Microbenchmark Harness (JMH)
提供了强大的基准测试能力,但是实际的开发中使用的并不多,很多时候会使用简单的接口性能对比,然后就可以了,并没有深入的分析每个放的性能具体是多少。
编写更多的性能测试代码显然是需要花费更多的时间精力,但个人觉得是很有必要的,在项目不那么紧张的情况下,建议大家都尝试使用JMH对性能进行一定的研究和深入,这样对于提升自己的能力至关重要。