联博开奖_JMH - Java 代码性能测试的最终利器、必须掌握
发表时间:2021-12-22 浏览量:77
JMH - Java 代码性能测试的最终利器、必须掌握
JMH - Java 代码性能测试的最终利器、必须掌握
以下文章来源于未读代码 ,作者达西呀
未读代码
一线手艺工具人的学习、生涯与见闻。
Java 性能测试难题
现在的 JVM 已经越来越为智能,它可以在编译阶段、加载阶段、运行阶段对代码举行优化。好比你写了一段不怎么伶俐的代码,到了 JVM 这里,它发现几处可以优化的地方,就随手帮你优化了一把。这对程序的运行虽然美妙,却让开发者不能准确领会程序的运行情形。在需要举行性能测试时,若是不知道 JVM 优化细节,可能会导致你的测试效果差之毫厘,失之千里,同样的,Java 降生之初就有一次编译、随处运行的口号,JVM 提供了底层支持,也提供了内存管理机制,这些机制都市对我们的性能测试效果造成不能展望的影响。
long start = System.currentTimeMillis();// ....long end = System.currentTimeMillis();System.out.println(end - start);
上面可能就是你最常见的性能测试了,这样的测试效果真的准确吗?谜底是否认的,它有下面几个问题。
时间精度问题,自己获取到的时间戳就是存在误差的,它和操作系统有关。
JVM 在运行时会举行代码预热,说白了就是越跑越快。由于类需要装载、需要准备操作。
JVM 会在各个阶段都有可能对你的代码举行优化处置。
资源接纳的不确定性,可能运行很快,接纳很慢。
带着这些问题,突然发现举行一次严酷的基准测试的难度大大增添。那么若何才气举行一次严酷的基准测试呢?
JMH 先容
那么若何对 Java 程序举行一次精准的性能测试呢?岂非需要掌握许多 JVM 优化细节吗?岂非要研究若何制止,并举行准确编码才气举行严酷的性能测试吗?显然不是,若是是这样的话,未免过于困难了,幸亏有一款一款官方的微基准测试工具 - JMH.
JMH 的全名是 Java Microbenchmark Harness,它是由 Java 虚拟机团队开发的一款用于 Java 微基准测试工具。用自己开发的工具测试自己开发的另一款工具,以子之矛,攻子之盾果真手到擒来,如臂使指。使用 JMH 可以让你利便快速的举行一次严酷的代码基准测试,并且有多种测试模式,多种测试维度可供选择;而且使用简朴、增添注解便可启动测试。
JMH 使用
JMH 的使用首先引入 maven 所需依赖,当前最新版 为 1.23 版本。
<!--jmh 基准测试 --><dependency> <groupId>org.openjdk.jmh</groupId> <artifactId>jmh-core</artifactId> <version>1.23</version></dependency><dependency> <groupId>org.openjdk.jmh</groupId> <artifactId>jmh-generator-annprocess</artifactId> <version>1.23</version> <scope>provided</scope></dependency>
快速测试
下面使用注解的方式指定测试参数,通过一个例子展示 JMH 基准测试的详细用法,先看一次运行效果,然后再领会每个注解的详细寄义。
这个例子是使用 JMH 测试,使用加号拼接字符串和使用 StringBuilder
的append
方式拼接字符串时的速率若何,每次拼接1000个数字举行平均速率对照。
import java.util.concurrent.TimeUnit;
import org.openjdk.jmh.annotations.*;
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;
/**
* <p>
* JMH 基准测试入门
*
* @author niujinpeng
* @Date 2020/8/21 1:13
*/
@BenchmarkMode(Mode.AverageTime)
@State(Scope.Thread)
@Fork(1)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Warmup(iterations = 3)
@Measurement(iterations = 5)
public class JmhHello {
String string = "";
StringBuilder stringBuilder = new StringBuilder();
@Benchmark
public String stringAdd() {
for (int i = 0; i < 1000; i++) {
string = string + i;
}
return string;
}
@Benchmark
public String stringBuilderAppend() {
for (int i = 0; i < 1000; i++) {
stringBuilder.append(i);
}
return stringBuilder.toString();
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JmhHello.class.getSimpleName())
.build();
new Runner(opt).run();
}
}
代码很简朴,不做注释,stringAdd
使用加号拼接字符串 1000次,stringBuilderAppend
使用 append
拼接字符串 1000次。直接运行 main 方式,稍等片刻后可以获得详细的运行输出效果。
// 最先测试 stringAdd 方式
# JMH version: 1.23
# VM version: JDK 1.8.0_181, Java HotSpot(TM) 64-Bit Server VM, 25.181-b13
# VM invoker: D:\develop\Java\jdk8_181\jre\bin\java.exe
# VM options: -javaagent:C:\ideaIU-2020.1.3.win\lib\idea_rt.jar=50363:C:\ideaIU-2020.1.3.win\bin -Dfile.encoding=UTF-8
# Warmup: 3 iterations, 10 s each // 预热运行三次
# Measurement: 5 iterations, 10 s each // 性能测试5次
# Timeout: 10 min per iteration // 超时时间10分钟
# Threads: 1 thread, will synchronize iterations // 线程数目为1
# Benchmark mode: Average time, time/op // 统计方式挪用一次的平均时间
# Benchmark: net.codingme.jmh.JmhHello.stringAdd // 本次执行的方式
# Run progress: 0.00% complete, ETA 00:02:40
# Fork: 1 of 1
# Warmup Iteration 1: 95.153 ms/op // 第一次预热,耗时95ms
# Warmup Iteration 2: 108.927 ms/op // 第二次预热,耗时108ms
# Warmup Iteration 3: 167.760 ms/op // 第三次预热,耗时167ms
Iteration 1: 198.897 ms/op // 执行五次耗时器量
Iteration 2: 243.437 ms/op
Iteration 3: 271.171 ms/op
Iteration 4: 295.636 ms/op
Iteration 5: 327.822 ms/op
Result "net.codingme.jmh.JmhHello.stringAdd":
267.393 ±(99.9%) 189.907 ms/op [Average]
(min, avg, max) = (198.897, 267.393, 327.822), stdev = 49.318 // 执行的最小、平均、最大、误差值
CI (99.9%): [77.486, 457.299] (assumes normal distribution)
// 最先测试 stringBuilderAppend 方式
# Benchmark: net.codingme.jmh.JmhHello.stringBuilderAppend
# Run progress: 50.00% complete, ETA 00:01:21
# Fork: 1 of 1
# Warmup Iteration 1: 1.872 ms/op
# Warmup Iteration 2: 4.491 ms/op
# Warmup Iteration 3: 5.866 ms/op
Iteration 1: 6.936 ms/op
Iteration 2: 8.465 ms/op
Iteration 3: 8.925 ms/op
Iteration 4: 9.766 ms/op
Iteration 5: 10.143 ms/op
Result "net.codingme.jmh.JmhHello.stringBuilderAppend":
8.847 ±(99.9%) 4.844 ms/op [Average]
(min, avg, max) = (6.936, 8.847, 10.143), stdev = 1.258
CI (99.9%): [4.003, 13.691] (assumes normal distribution)
# Run complete. Total time: 00:02:42
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
JmhHello.stringAdd avgt 5 267.393 ± 189.907 ms/op
JmhHello.stringBuilderAppend avgt 5 8.847 ± 4.844 ms/op
Process finished with exit code 0
上面日志里的 //
注释是我手动增添上去的,实在我们只需要看下面的最终效果就可以了,可以看到 stringAdd
方式平均耗时 267.393ms,而 stringBuilderAppend
方式平均耗时只有 8.847ms,可见 StringBuilder
的append
方式举行字符串拼接速率快的多,这也是我们推荐使用append
举行字符串拼接的缘故原由。
注解说明
经由上面的示例,想必你也可以快速的使用 JMH 举行基准测试了,不外上面的诸多注解你可能另有疑惑,下面逐一先容。
类上使用了六个注解。
@BenchmarkMode(Mode.AverageTime)@State(Scope.Thread)@Fork(1)@OutputTimeUnit(TimeUnit.MILLISECONDS)@Warmup(iterations = 3)@Measurement(iterations = 5)
@BenchmarkMode(Mode.AverageTime) 示意统计平均响应时间,不仅可以用在类上,也可用在测试方式上。
除此之外还可以取值:
Throughput:统计单元时间内可以对方式测试多少次。
SampleTime:统计每个响应时间局限内的响应次数,好比 0-1ms,3次;1-2ms,5次。
,
,www.9cx.net采用以太坊区块链高度哈希值作为统计数据,联博以太坊统计数据开源、公平、无任何作弊可能性。联博统计免费提供API接口,支持多语言接入。
SingleShotTime:跳过预热阶段,直接举行一次****微基准测试。
@State(Scope.Thread):每个举行基准测试的线程都市独享一个工具示例。
除此之外还能取值:
Benchmark:多线程共享一个示例。
Group:线程组共享一个示例,在测试方式上使用 @Group 设置线程组。
@Fork(1):示意开启一个线程举行测试。
**OutputTimeUnit(TimeUnit.MILLISECONDS):输出的时间单元,这里写的是毫秒。
@Warmup(iterations = 3):微基准测试前举行三次预热执行,也可用在测试方式上。
@Measurement(iterations = 5):举行 5 次微基准测试,也可用在测试方式上。
在两个测试方式上只使用了一个注解 @Benchmark,这个注解示意这个方式是要举行基准测试的方式,它类似于 Junit 中的 @Test 注解。上面还提到某些注解还可以用到测试方式上,也就是使用了 @Benchmark 的方式之上,若是类上和测试方式同时存在注解,会以方式上的注解为准。
实在 JMH 也可以把这些参数直接在 main 方式中指定,这时 main 方式中指定的级别最高。
public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder() .include(JmhHello.class.getSimpleName()) .forks(1) .warmupIterations(5) .measurementIterations(10) .build(); new Runner(opt).run();}
准确的微基准测试
若是编写的代码自己就存在着诸多问题,那么纵然使用准确的测试方式,也不能能获得准确的测试效果。这些测试代码中的问题应该由我们举行自动制止,那么有哪些常见问题呢?下面先容两种最常见的情形。
无用代码消除 ( Dead Code Elimination )
也有网友形象的翻译成死代码,死代码是指那些 JVM 经由检查发现的基本不会使用到的代码。好比下面这个代码片断。
import java.util.concurrent.TimeUnit;
import org.openjdk.jmh.annotations.*;
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;
/**
* <p>
* 测试死代码消除
*
* @author niujinpeng
* @Date 2020/8/21 8:04
*/
@BenchmarkMode(Mode.AverageTime)
@State(Scope.Thread)
@Fork(1)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Warmup(iterations = 3, time = 3)
@Measurement(iterations = 5, time = 3)
public class JmhDCE {
@Benchmark
public double test1() {
return Math.log(Math.PI);
}
@Benchmark
public void test2() {
double result = Math.log(Math.PI);
result = Math.log(result);
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JmhDCE.class.getSimpleName())
.build();
new Runner(opt).run();
}
}
在这个代码片断里里,test1
方式对圆周率举行对数盘算,并返回盘算效果;而 test2
中不仅对圆周率举行对数盘算,还对盘算的效果再次对数盘算,看起来庞大一些,然则由于没有用到盘算效果,以是 JVM 会自动消除这段代码, 由于它没有任何意义。
Benchmark Mode Cnt Score Error UnitsJmhDCE.test1 avgt 5 0.002 ± 0.001 us/opJmhDCE.test2 avgt 5 ≈ 10⁻⁴ us/op
测试效果里也可以看到 test
平均耗时 0.0004 微秒,而 test1
平均耗时 0.002 微秒。
常量折叠 (Constant Folding)
在对 Java 源文件编译的历程中,编译器通过语法分析,可以发现某些能直接获得盘算效果而不会再次更改的代码,然后会将盘算效果记录下来,这样在执行的历程中就不需要再次运算了。好比这段代码。
import java.util.concurrent.TimeUnit;
import org.openjdk.jmh.annotations.*;
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;
/**
* <p>
* 测试常量折叠
*
* @author niujinpeng
* @Date 2020/8/21 8:23
*/
@BenchmarkMode(Mode.AverageTime)
@State(Scope.Thread)
@Fork(1)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Warmup(iterations = 3, time = 3)
@Measurement(iterations = 5, time = 3)
public class JmhConstantFolding {
final double PI1 = 3.14159265358979323846;
double PI2 = 3.14159265358979323846;
@Benchmark
public double test1() {
return Math.log(PI1) * Math.log(PI1);
}
@Benchmark
public double test2() {
return Math.log(PI2) * Math.log(PI2);
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder().include(JmhConstantFolding.class.getSimpleName()).build();
new Runner(opt).run();
}
}
test
1 中使用 final
修饰的 PI1 举行工具盘算,由于 PI1 不能再次更改,以是 test1
的盘算效果必定是不会更改的,以是 JVM 会举行常量折叠优化,而 test2
使用的 PI2
可能会被修改,以是只能每次举行盘算。
Benchmark Mode Cnt Score Error UnitsJmhConstantFolding.test1 avgt 5 0.002 ± 0.001 us/opJmhConstantFolding.test2 avgt 5 0.019 ± 0.001 us/op
可以看到 test2
耗时要多的多,达到了 0.019 微秒。
实在 JVM 做的优化操作远不止上面这些,另有好比常量流传(Constant Propagation)、循环展开(Loop Unwinding)、循环表达式外提(Loop Expression Hoisting)、消除公共子表达式(Common Subexpression Elimination)、本块重排序(Basic Block Reordering)、局限检查消除(Range Check Elimination)等。
总结
JMH 举行基准测试的使用历程并不庞大,同为 Java 虚拟机团队开发,准确性毋容置疑。然则在举行基准测试时照样要注意自己的代码问题,若是编写的要举行测试的代码自己存在问题,那么测试的效果必定是禁绝的。掌握了 JMH 基准测试之后,可以实验测试一些常用的工具或者框架的性能若何,看看哪个工具的性能最好,好比 FastJSON 真的比 GSON 在举行 JSON 转换时更 Fast 吗?Spring 的 BeanUtils 和 Apache 的 BeanUtils 哪个速率更快?
参考:
https://www.ibm.com/developerworks/cn/java/j-benchmark1.html
http://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/
深入明白Java虚拟机:JVM高级特征与最佳实践(第3版)第11章 后端编译与优化
一个「在看」,一段时光
0
珍藏