环境
MacBook Pro
Java 1.8
前言
无意中看到JMH – 用来测试方法性能的东东。
好吧,我就了解下~ 未来说不定用得到。
JMH
名字就是标题上名称:Java 微基准测试套件
。
英文名叫:Java Microbenchmark Harness
JMH
Java9中作为JDK的一部分已经引入了;
但是我是Java8,又是gradle项目;需要引入Open-jdk中相应的依赖才行。
build.gradle
在 build.gradle 文件中添加:
compile group: 'org.openjdk.jmh', name: 'jmh-core', version: '1.23'
compile('org.openjdk.jmh:jmh-generator-annprocess:1.23')
就可以了,接下来就是写代码了
写测试代码
package jmh;
import org.openjdk.jmh.annotations.Benchmark;
/**
* @author yutao
* @since 2020/6/3 5:00 下午
*/
public class StringConnectBenchmark {
@Benchmark
public void testStringBuilder() {
print(new StringBuilder().append(1).append(2).append(3).toString());
}
@Benchmark
public void testStringAdd() {
print(new String() + 1 + 2 + 3);
}
@Benchmark
public void testStringConcat() {
print(new String().concat("1").concat("2").concat("3"));
}
@Benchmark
public void testStringBuffer() {
print(new StringBuffer().append(1).append(2).append(3).toString());
}
@Benchmark
public void testStringFormat() {
print(String.format("%s%s%s", 1, 2, 3));
}
private void print(String toString) {
}
}
启动代码
package jmh;
import org.openjdk.jmh.annotations.Mode;
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.OptionsBuilder;
/**
* @author yutao
* @since 2020/6/3 5:00 下午
*/
public class StringBuilderRunner {
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder().include(StringConnectBenchmark.class.getSimpleName())
.warmupIterations(5)
.measurementIterations(10)
.mode(Mode.Throughput)
.forks(3)
.build();
new Runner(opt).run();
}
}
执行这个main
方法就可以了。
遇到错误
ERROR: Unable to find the resource: /META-INF/BenchmarkList
假设遇到上面的错误,可能是下面这个依赖的范围没有控制好。:
compile('org.openjdk.jmh:jmh-generator-annprocess:1.23')
比如我之前写的是:
testCompile('org.openjdk.jmh:jmh-generator-annprocess:1.23')
// 或
providedCompile('org.openjdk.jmh:jmh-generator-annprocess:1.23')
上面两种写法都会报那个错误,至少我是
结果
正常情况下,应该会有如下结果:
# JMH version: 1.23
# VM version: JDK 1.8.0_211, Java HotSpot(TM) 64-Bit Server VM, 25.211-b12
# VM invoker: /Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/bin/java
# VM options: -javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=57632:/Applications/IntelliJ IDEA.app/Contents/bin -Dfile.encoding=UTF-8
# Warmup: 5 iterations, 10 s each
# Measurement: 10 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: jmh.StringConnectBenchmark.testStringFormat
# Run progress: 80.00% complete, ETA 00:07:48
# Fork: 1 of 3
# Warmup Iteration 1: 1016531.265 ops/s
# Warmup Iteration 2: 1046406.204 ops/s
# Warmup Iteration 3: 1107229.841 ops/s
# Warmup Iteration 4: 1055051.289 ops/s
# Warmup Iteration 5: 1044207.190 ops/s
Iteration 1: 1127695.392 ops/s
Iteration 2: 1034626.224 ops/s
Iteration 3: 887208.217 ops/s
Iteration 4: 1041210.542 ops/s
Iteration 5: 1071716.734 ops/s
Iteration 6: 1024996.131 ops/s
Iteration 7: 1062593.555 ops/s
Iteration 8: 1087824.436 ops/s
Iteration 9: 1101977.356 ops/s
Iteration 10: 1166143.528 ops/s
# Run progress: 86.67% complete, ETA 00:05:12
# Fork: 2 of 3
# Warmup Iteration 1: 1197293.287 ops/s
# Warmup Iteration 2: 985332.916 ops/s
# Warmup Iteration 3: 1071488.561 ops/s
# Warmup Iteration 4: 1171514.666 ops/s
# Warmup Iteration 5: 807647.157 ops/s
Iteration 1: 984665.944 ops/s
Iteration 2: 1024186.487 ops/s
Iteration 3: 1140789.396 ops/s
Iteration 4: 1085807.609 ops/s
Iteration 5: 950306.564 ops/s
Iteration 6: 644449.235 ops/s
Iteration 7: 914517.484 ops/s
Iteration 8: 925476.242 ops/s
Iteration 9: 901025.908 ops/s
Iteration 10: 948277.296 ops/s
# Run progress: 93.33% complete, ETA 00:02:36
# Fork: 3 of 3
# Warmup Iteration 1: 999728.053 ops/s
# Warmup Iteration 2: 819815.425 ops/s
# Warmup Iteration 3: 906965.258 ops/s
# Warmup Iteration 4: 880806.121 ops/s
# Warmup Iteration 5: 851683.139 ops/s
Iteration 1: 900068.389 ops/s
Iteration 2: 844397.005 ops/s
Iteration 3: 1083297.259 ops/s
Iteration 4: 1091724.838 ops/s
Iteration 5: 1119853.012 ops/s
Iteration 6: 1153083.290 ops/s
Iteration 7: 1163449.184 ops/s
Iteration 8: 1150650.891 ops/s
Iteration 9: 1163546.343 ops/s
Iteration 10: 1018446.675 ops/s
Result "jmh.StringConnectBenchmark.testStringFormat":
1027133.706 ±(99.9%) 79567.642 ops/s [Average]
(min, avg, max) = (644449.235, 1027133.706, 1166143.528), stdev = 119093.109
CI (99.9%): [947566.064, 1106701.347] (assumes normal distribution)
# Run complete. Total time: 00:39:01
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
StringConnectBenchmark.testStringAdd thrpt 30 35317702.705 ± 1336860.556 ops/s
StringConnectBenchmark.testStringBuffer thrpt 30 94176455.257 ± 24771740.088 ops/s
StringConnectBenchmark.testStringBuilder thrpt 30 88236481.722 ± 12615288.187 ops/s
StringConnectBenchmark.testStringConcat thrpt 30 41883416.348 ± 2917355.393 ops/s
StringConnectBenchmark.testStringFormat thrpt 30 1027133.706 ± 79567.642 ops/s
Process finished with exit code 0
结果分析
这个结果怎么看呢?
我们先来看下代码的配置:
结果主要由四部分组成
① Warmup Iteration 1: 1016531.265 ops/s 这种格式数据
这就是预热数据执行的结果
② Iteration 5: 1119853.012 ops/s 这种格式的数据
这部分是正式基准测试迭代的结果
③ Result … … 这个格式数据
这部分就是数据跑完了最后结果。
比如:(min, avg, max) = (644449.235, 1027133.706, 1166143.528)
最小值,平均值,最大值。
④ Run complete. Total time 这中格式的数据
Benchmark | Mode | Cnt | Score | Error | Units |
---|---|---|---|---|---|
StringConnectBenchmark.testStringAdd | thrpt | 30 | 35317702.705 ± 1336860.556 | ops/s | |
StringConnectBenchmark.testStringBuffer | thrpt | 30 | 94176455.257 ± 24771740.088 | ops/s | |
StringConnectBenchmark.testStringBuilder | thrpt | 30 | 88236481.722 ± 12615288.187 | ops/s | |
StringConnectBenchmark.testStringConcat | thrpt | 30 | 41883416.348 ± 2917355.393 | ops/s | |
StringConnectBenchmark.testStringFormat | thrpt | 30 | 1027133.706 ± 79567.642 | ops/s |
其中Cnt 是count的缩写
默认场景下,JMH 会找寻标注了@Benchmark类型的方法,可能会跑一些你所不需要的测试,这样就需要通过include和exclude两个方法来完成包含以及排除的语义。 我这里指定就是StringConnectBenchmark
类来跑测试。
名称 | 含义 |
---|---|
warmupIterations | 预热次数 |
measurementIterations | 正式测试次数 |
mode | 使用的模式 |
forks | 工做几轮测试 |
All | 顾名思义,所有模式,这个在内部测试中常用 |
每轮正式之前,都会执行预热
测试类型mode有:
测试类型 | 含义 |
---|---|
Throughput | 吞吐量 |
AverageTime | 平均时间 |
SampleTime | 在测试时,随机采样执行的时间 |
SingleShotTime | 测量单次操作的时间,通过调用Benchmark一次并测量其时间 |
使用这些模式也非常简单,只需要增加@BenchmarkMode注解即可,例如:
@Benchmark
@BenchmarkMode({Mode.Throughput, Mode.SingleShotTime})
public void m() {
}
为什么要预热呢?
在使用Java
编程过程中,我们对于一些代码调用的细节有多种编写方式,但是不确定它们性能时,往往采用重复多次计数的方式来解决。但是随着JVM
不断的进化,随着代码执行次数的增加,JVM
会不断的进行编译优化,使得重复多少次才能够得到一个稳定的测试结果变得让人疑惑,这时候有经验的同学就会在测试执行前先循环上万次并注释为预热。
没错!这样做确实可以获得一个偏向正确的测试结果,但是我们试想如果每到需要斟酌性能的时候,都要根据场景写一段预热的逻辑吗?当预热完成后,需要多少次迭代来进行正式内容的测量呢?每次测试结果的输出报告是不是都需要用System.out
来输出呢?
其实这些工作都可以交给 JMH (the Java Microbenchmark Harness)
,它被作为Java9
的一部分来发布,但是我们完全不需要等待Java9
,而可以方便的使用它来简化我们测试,它能够照看好JVM
的预热、代码优化,让你的测试过程变得更加简单。
参考地址: