性能测试JMH
JMH,即(Java Microbenchmark Harness) 用于代码微基准测试的工具套件,主要是基于方法层面的基准测试,精度可以达到纳秒级。 基准测试:是指通过设计科学的测试方法、测试工具和测试系统,实现对一类测试对象的某项性能指标进行定量的和可对比的测试。
-
micro英[ˈmaɪkrəʊ]微观的;
单机压测工具JMH,2013年由oracle内部JIT的大牛们开发,归于OpenJDK
性能测试生成图的网站:http://deepoove.com/jmh-visual-chart/ (偶尔会故障)
注:JDK9后自带JMK
JMH 比较典型的应用场景如下:
想准确地知道某个方法需要执行多长时间,以及执行时间和输入之间的相关性
对比接口不同实现在给定条件下的吞吐量
查看多少百分比的请求在多长时间内完成
JMH入门
maven依赖
<!--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>
性能测试代码
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启动测试。
测试结果:
# JMH version: 1.29 # VM version: JDK 1.8.0_281, Java HotSpot(TM) 64-Bit Server VM, 25.281-b09 # VM invoker: C:\MySoftwear\Java\jdk1.8\jre\bin\java.exe # VM options: -javaagent:C:\MySoftwear\Java\ide\IntelliJ IDEA 2020.3.2\lib\idea_rt.jar=53484:C:\MySoftwear\Java\ide\IntelliJ IDEA 2020.3.2\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: 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,具体配置如下:
作者:武培轩 链接: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 包并执行:
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、代码死码消除
@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
了解全部的陷阱。