大家好,又见面了,我是你们的朋友全栈君。
性能测试JMH
JMH,即(Java Microbenchmark Harness) 用于代码微基准测试的工具套件,主要是基于方法层面的基准测试,精度可以达到纳秒级。 基准测试:是指通过设计科学的测试方法、测试工具和测试系统,实现对一类测试对象的某项性能指标进行定量的和可对比的测试。
- micro英[ˈmaɪkrəʊ]微观的;
单机压测工具JMH,2013年由oracle内部JIT的大牛们开发,归于OpenJDK
性能测试生成图的网站:http://deepoove.com/jmh-visual-chart/ (偶尔会故障)
注:JDK9后自带JMK
JMH 比较典型的应用场景如下:
- 想准确地知道某个方法需要执行多长时间,以及执行时间和输入之间的相关性
- 对比接口不同实现在给定条件下的吞吐量
- 查看多少百分比的请求在多长时间内完成
JMH入门
maven依赖
代码语言:javascript复制<!--Benchmark基准测试-->
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.29</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.29</version>
<scope>test</scope>
</dependency>
性能测试代码
代码语言:javascript复制package org.dh.nicetuantools.service;
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.OptionsBuilde
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 JMHeduTest {
// 判断 和 StringBuilder.append() 两种字符串拼接哪个耗时更短
@Param(value = {"10", "50", "100"})
private int length;
@Benchmark
public void testStringAdd(Blackhole blackhole) {
String a = "";
for (int i = 0; i < length; i ) {
a = i;
}
blackhole.consume(a);
}
@Benchmark
public void testStringBuilderAdd(Blackhole blackhole) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i ) {
sb.append(i);
}
blackhole.consume(sb.toString());
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JMHeduTest.class.getSimpleName())
.result("result.json")
.resultFormat(ResultFormatType.JSON).build();
new Runner(opt).run();
}
}
【说明】:
@Benchmark
注解标识的方法是要测试的方法 main()函数中首先对测试用例进行配置,使用Builder模式配置测试,配置参数存入Options, 并用Options构造Runner启动测试。
测试结果:
代码语言:javascript复制# JMH version: 1.29
# VM version: JDK 1.8.0_281, Java HotSpot(TM) 64-Bit Server VM, 25.281-b09
# VM invoker: C:MySoftwearJavajdk1.8jrebinjava.exe
# VM options: -javaagent:C:MySoftwearJavaideIntelliJ IDEA 2020.3.2libidea_rt.jar=53484:C:MySoftwearJavaideIntelliJ IDEA 2020.3.2bin -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: org.dh.nicetuantools.service.JMHeduTest.testStringAdd
# Parameters: (length = 10)
该部分为测试的基本信息,如java路径,预热代码迭代次数,测试代码迭代次数,使用线程数,测试统计单位
# Run progress: 0.00% complete, ETA 00:02:48
# Fork: 1 of 1
# Warmup Iteration 1: 354.206 ±(99.9%) 28.636 ns/op
# Warmup Iteration 2: 283.927 ±(99.9%) 59.079 ns/op
# Warmup Iteration 3: 254.795 ±(99.9%) 50.735 ns/op
该部分为每一次热身中的性能指标,预热测试不会作为最终的统计结果。预热的目的是让 JVM 对被测代码进行足够多的优化,比如,在预热后,被测代码应该得到了充分的 JIT 编译和优化。
Iteration 1: 256.655 ±(99.9%) 25.304 ns/op
Iteration 2: 256.456 ±(99.9%) 7.752 ns/op
Iteration 3: 272.966 ±(99.9%) 19.430 ns/op
Iteration 4: 278.945 ±(99.9%) 9.152 ns/op
Iteration 5: 271.370 ±(99.9%) 11.790 ns/op
Result "org.dh.nicetuantools.service.JMHeduTest.testStringAdd":
267.278 ±(99.9%) 39.229 ns/op [Average]
(min, avg, max) = (256.456, 267.278, 278.945), stdev = 10.188
CI (99.9%): [228.049, 306.508] (assumes normal distribution)
Benchmark (length) Mode Cnt Score Error Units
JMHeduTest.testStringAdd 100 avgt 5 10933.988 ± 923.095 ns/op
该部分显示测量迭代的情况,每一次迭代都显示了当前的执行速率,即一个操作所花费的时间。在进行 5 次迭代后,进行统计,在本例中,length 为 100 的情况下 testStringBuilderAdd 方法的平均执行花费时间为 10933.988 ns,误差为 923.095 ns。
最后的测试结果如下所示:
Benchmark (length) Mode Cnt Score Error Units
JMHeduTest.testStringAdd 10 avgt 5 267.278 ± 39.229 ns/op
JMHeduTest.testStringAdd 50 avgt 5 2950.072 ± 39.555 ns/op
JMHeduTest.testStringAdd 100 avgt 5 10933.988 ± 923.095 ns/op
JMHeduTest.testStringBuilderAdd 10 avgt 5 175.045 ± 70.243 ns/op
JMHeduTest.testStringBuilderAdd 50 avgt 5 1002.491 ± 671.179 ns/op
JMHeduTest.testStringBuilderAdd 100 avgt 5 2523.618 ± 254.709 ns/op
结果表明,在拼接字符次数越多的情况下,StringBuilder.append() 的性能就更好。
另外一种执行方式在 maven 里增加一个 plugin,具体配置如下:
代码语言:javascript复制作者:武培轩
链接:https://www.zhihu.com/question/276455629/answer/1259967560
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
<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>
接着执行 maven 的命令生成可执行 jar 包并执行:
代码语言:javascript复制mvn clean install
java -jar target/jmh-demo.jar JMHeduTest
Benchmark | Mode | Cnt | Score | Error | Units |
---|---|---|---|---|---|
基准测试执行的方法 | 测试模式 | 运行多少次 | 分数 | 错误 | 单位 |
常见注解
@BenchmarkMode用来配置 Mode 选项,可用于类或者方法上,这个注解的 value 是一个数组,可以把几种 Mode 集合在一起执行,如:@BenchmarkMode({Mode.SampleTime, Mode.AverageTime})
,还可以设置为 Mode.All
,即全部执行一遍。
@Warmup预热所需要配置的一些基本测试参数,可用于类或者方法上。一般前几次进行程序测试的时候都会比较慢,所以要让程序进行几轮预热,保证测试的准确性。 参数如下所示: 1.iterations:预热的次数 2.Time :每次预热的时间 3.timeUnit:时间的单位,默认秒 4.batchSize: 批处理大小,每次操作调用几次方法
@Measurement实际调用方法所需要配置的一些基本测试参数,可用于类或者方法上,参数和 @Warmup
相同。
@Threads每个进程中的测试线程,可用于类或者方法上。
@Fork进行 fork 的次数,可用于类或者方法上。如果 fork 数是 2 的话,则 JMH 会 fork 出两个进程来进行测试。
@State通过 State 可以指定一个对象的作用范围,JMH 根据 scope 来进行实例化和共享操作。@State 可以被继承使用,如果父类定义了该注解,子类则无需定义。由于 JMH 允许多线程同时执行测试,不同的选项含义如下: 1.Scope.Benchmark:所有测试线程共享一个实例,测试有状态实例在多线程共享下的性能 2.Scope.Group:同一个线程在同一个 group 里共享实例 3.Scope.Thread:默认的 State,每个测试线程分配一个实例
@OutputTimeUnit 为统计结果的时间单位,可用于类或者方法注解
@Benchmark:标记需要测试的方法
@Param 指定某项参数的多种情况,特别适合用来测试一个函数在不同的参数输入的情况下的性能,只能作用在字段上,使用该注解必须定义 @State 注解
测试模式(Mode)
@BenchmarkMode注解可选项
1、Throughput: 整体吞吐量, 表示1秒内可以执行多少次调用,单位为 ops/time
2、AverageTime: 调用的平均时间, 每次操作的平均时间,单位为 time/op
3、SampleTime:随机取样,最后输出取样结果的分布,例如“99%的调用在XXX 毫秒以内,99.99%的调用在XXX 毫秒以内”
4、SingleShotTime: 以上模式都是默认一次Iteration是1秒,唯有SingleShotTime 只运行一次。往往同时把warmup 次数设为0, 用于测试冷启动时的性能。
5、All : 上面的所有模式都执行一次
配置类(Options/OptionsBuilder)
使用 Builder 模式配置测试,将配置参数存入 Options 对象,并使用 Options 对象构造 Runner 启动测试。
OptionsBuilder的常用方法及对应的注解形式如下:Options opt = new OptionsBuilder()
启动 new Runner(opt).run();
方法名 | 参数 | 作用 | 对应注解 |
---|---|---|---|
include | 接受一个字符串表达式,表示需要测试的类和方法。 | <div style=”width:200″>指定要运行的基准测试类和方</div> | – |
exclude | 接受一个字符串表达式,表示不需要测试的类和方法 | 指定不要运行的基准测试类方法 | – |
warmupIterations | 预热的迭代次数 | 指定预热的迭代次数 | @Warmup |
warmupBatchSize | 预热批量的大小 | 指定预热批量的大小 | @Warmup |
warmupForks | 预热模式:INDI,BULK,BULK_INDI | 指定预热模式 | @Warmup |
warmupMode | 预热的模式 | 指定预热的模式 | @Warmup |
warmupTime | 预热的时间 | 指定预热的时间 | @Warmup |
measurementIterations | 测试的迭代次数 | 指定测试的迭代次数 | @Measurement |
measurementBatchSize | 测试批量的大小 | 指定测试批量的大小 | @Measurement |
measurementTime | 测试的时间 | 指定测试的时间 | @Measurement |
mode | 测试模式: Throughput(吞吐量), AverageTime(平均时间),SampleTime(在测试中,随机进行采样执行的时间),SingleShotTime(在每次执行中计算耗时),All(所有) | 指定测试模式 | @BenchmarkMode–可用于类或者方法上 |
Fork | 子进程数 | ||
threads | 每个方法开启线程数量 | 多线程测试 | @Threads,可用在方法或者类上 |
2.6 其他注解 @OutputTimeUnit benchmark 结果所使用的时间单位,可用于类或者方法注解,使用java.util.concurrent.TimeUnit中的标准时间单位。 @Setup 方法注解,会在执行 benchmark 之前被执行,正如其名,主要用于初始化。 @TearDown 方法注解,与@Setup相对的,会在所有benchmark执行结束以后执行,主要用于资源的回收等。 @Param 成员注解,可以用来指定某项参数的多种情况。特别适合用来测试一个函数在不同的参数输入的情况下的性能。@Param注解接收一个String数组,在@setup方法执行前转化为为对应的数据类型。多个@Param注解的成员之间是乘积关系,譬如有两个用@Param注解的字段,第一个有5个值,第二个字段有2个值,那么每个测试方法会跑5*2=10次。
注意事项
1、代码死码消除
代码语言:javascript复制@Benchmark
public void testStringAdd(Blackhole blackhole) {
String a = "";
for (int i = 0; i < length; i ) {
a = i;
}
}
JVM 可能会认为变量 a
从来没有使用过,从而进行优化把整个方法内部代码移除掉,这就会影响测试结果。 JMH 提供了两种方式避免这种问题,一种是将这个变量作为方法返回值 return a,一种是通过 Blackhole 的 consume 来避免 JIT 的优化消除。
其他陷阱还有常量折叠与常量传播、永远不要在测试中写循环、使用 Fork 隔离多个测试方法、方法内联、伪共享与缓存行、分支预测、多线程测试等,感兴趣的可以阅读 https://github.com/lexburner/JMH-samples
了解全部的陷阱。
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/157691.html原文链接:https://javaforall.cn