前言:
当前各位老铁们对“javahhhh”都比较讲究,兄弟们都想要了解一些“javahhhh”的相关文章。那么小编也在网摘上搜集了一些对于“javahhhh””的相关文章,希望咱们能喜欢,大家快快来了解一下吧!JMH简介
JMH(Java Microbenchmark Harness)是用于代码微基准测试的工具套件,主要是基于方法层面的基准测试,精度可以达到纳秒级。
JMH 比较典型的应用场景如下:
想准确地知道某个方法需要执行多长时间,以及执行时间和输入之间的相关性对比接口不同实现在给定条件下的吞吐量查看多少百分比的请求在多长时间内完成
下面我们以 FastJson 和 Jackson 为例使用 JMH 做基准测试。
引入依赖
<dependency> <groupId>org.openjdk.jmh</groupId> <artifactId>jmh-core</artifactId> <version>1.28</version></dependency><dependency> <groupId>org.openjdk.jmh</groupId> <artifactId>jmh-generator-annprocess</artifactId> <version>1.28</version></dependency><dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.78</version></dependency><dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.12.5</version></dependency><dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.12.5</version></dependency>编写基准测试用例
package jmh;import com.alibaba.fastjson.JSON;import com.fasterxml.jackson.core.JsonProcessingException;import com.fasterxml.jackson.databind.ObjectMapper;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.OptionsBuilder;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 JSONSerializerTest { ObjectMapper objectMapper = new ObjectMapper(); @Param(value = {"10", "50", "100"}) private int times; @Benchmark public void testFastJson(Blackhole blackhole) { for (int i = 0; i < times; i++) { String user = JSON.toJSONString(new User()); blackhole.consume(user); } } @Benchmark public void testJackson(Blackhole blackhole) throws JsonProcessingException { for (int i = 0; i < times; i++) { String user = objectMapper.writeValueAsString(new User()); blackhole.consume(user); } } public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder() .include(JSONSerializerTest.class.getSimpleName()) .result("result.json") .resultFormat(ResultFormatType.JSON).build(); new Runner(opt).run(); }}
其中需要测试的方法用 @Benchmark 注解标识,这些注解的具体含义将在下面介绍。
在 main() 函数中,首先对测试用例进行配置,使用 Builder 模式配置测试,将配置参数存入 Options 对象,并使用 Options 对象构造 Runner 启动测试。
执行基准测试
执行 main 方法后,稍等片刻,便能输出全部测试结果。接下来分别查看每个日志的信息:
# JMH version: 1.28# VM version: JDK 1.8.0_231, Java HotSpot(TM) 64-Bit Server VM, 25.231-b11# VM invoker: C:\Program Files\Java\jdk1.8.0_231\jre\bin\java.exe# VM options: -javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2020.2.1\lib\idea_rt.jar=54352:C:\Program Files\JetBrains\IntelliJ IDEA 2020.2.1\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: jmh.JSONSerializerTest.testFastJson# Parameters: (times = 10)
可以看到JMH 版本、JVM 版本、线程数、测试方法、测试模式等测试参数信息。
# Warmup Iteration 1: 2762.740 ±(99.9%) 985.723 ns/op# Warmup Iteration 2: 2821.324 ±(99.9%) 273.518 ns/op# Warmup Iteration 3: 2854.380 ±(99.9%) 182.686 ns/op
此部分为每一次预热测试的性能指标,预热测试不会作为最终的统计结果。预热的目的是让 JVM 对被测代码进行足够多的优化,比如,在预热后,被测代码应该得到了充分的 JIT 编译和优化。
Iteration 1: 2775.728 ±(99.9%) 283.540 ns/opIteration 2: 2651.981 ±(99.9%) 150.255 ns/opIteration 3: 2668.217 ±(99.9%) 176.983 ns/opIteration 4: 2554.293 ±(99.9%) 181.387 ns/opIteration 5: 2616.313 ±(99.9%) 149.811 ns/opResult "jmh.JSONSerializerTest.testFastJson": 2653.306 ±(99.9%) 312.730 ns/op [Average] (min, avg, max) = (2554.293, 2653.306, 2775.728), stdev = 81.215 CI (99.9%): [2340.577, 2966.036] (assumes normal distribution)
该部分显示测量迭代的情况,每一次迭代都显示了当前的执行速率,即一个操作所花费的时间。在进行 5 次迭代后,进行统计,在本例中,times 为 10 的情况下 testFastJson 方法的平均执行耗时为 2653.306 ns,误差为 81.215 ns。
最后的测试对比结果如下:
Benchmark (times) Mode Cnt Score Error UnitsJSONSerializerTest.testFastJson 10 avgt 5 2653.306 ± 312.730 ns/opJSONSerializerTest.testFastJson 50 avgt 5 13242.378 ± 479.027 ns/opJSONSerializerTest.testFastJson 100 avgt 5 25789.098 ± 1017.915 ns/opJSONSerializerTest.testJackson 10 avgt 5 3801.418 ± 476.998 ns/opJSONSerializerTest.testJackson 50 avgt 5 18727.014 ± 531.329 ns/opJSONSerializerTest.testJackson 100 avgt 5 38515.980 ± 1700.508 ns/op
通过测试结果可知,FastJson 还是比 Jackson 快了许多的。
JMH基础介绍
1、@BenchmarkMode
JMH 可以在不同模式下运行您的基准测试。JMH 提供以下基准测试模式:
Throughput(吞吐量)
测量每秒的操作数,这意味着您的基准测试方法每秒可以执行的次数。
AverageTime(平均时间)
测量执行基准方法(单次执行)所需的平均时间。
SampleTime(采样时间)
随机取样,最后输出取样结果的分布。
SingleShotTime(单次时间)
测量单个基准方法执行运行所需的时间。一般用于测试在冷启动(没有 JVM 预热)下的性能。
ALL
以上所有措施。
2、@OutputTimeUnit
使用java.util.concurrent.TimeUnit作为参数进行使用。包括以下时间单位:
NANOSECONDSMICROSECONDSMILLISECONDSSECONDSMINUTESHOURSDAYS
3、@State
JMH 提供了可以重用状态对象的不同“范围”。状态范围在@State 注释的参数中指定。在上面的例子中,选择的范围是 Scope.Benchmark
Scope 类包含下列范围的常数:
Thread
运行基准测试的每个线程都将创建自己的状态对象实例。
Group
运行基准测试的每个线程组将创建自己的状态对象实例。
Benchmark
运行基准测试的所有线程共享相同的状态对象。
4、@Warmup
预热所需要配置的一些基本测试参数,可用于类或者方法上。一般前几次进行程序测试的时候都会比较慢,所以要让程序进行几轮预热,保证测试的准确性。参数如下所示:
iterations:预热的次数time:每次预热的时间timeUnit:时间的单位,默认秒batchSize:批处理大小,每次操作调用几次方法
5、@Measurement
实际调用方法所需要配置的一些基本测试参数,可用于类或者方法上,参数和 @Warmup 相同。
6、@Threads
每个进程中的测试线程数,可用于类或者方法上。如上示例中,使用的测试线程数为 4。
7、@Fork
进行 fork 的次数,可用于类或者方法上。如果 fork 数是 2 的话,则 JMH 会 fork 出两个进程来进行测试。
8、@Param
指定某项参数的多种情况,特别适合用来测试一个函数在不同的参数输入的情况下的性能,只能作用在字段上,使用该注解必须定义 @State 注解。
介绍完基础使用之后,我们再来看看两个经常要注意的情况。
常见问题
1、死代码消除
在运行微基准测试时,了解优化非常重要。否则,它们可能会影响基准测试结果,并且对我们产生误导。
参考一个例子:
@Benchmark@OutputTimeUnit(TimeUnit.NANOSECONDS)@BenchmarkMode(Mode.AverageTime)public void doNothing() {}@Benchmark@OutputTimeUnit(TimeUnit.NANOSECONDS)@BenchmarkMode(Mode.AverageTime)public void objectCreation() { new Object();}
在运行测试之前,我们肯定猜测第二个测试代码块会消耗更多的时间,毕竟它多创建了一个对象。实际测试结果如下:
Benchmark Mode Cnt Score Error UnitsBenchMark.doNothing avgt 40 0.609 ± 0.006 ns/opBenchMark.objectCreation avgt 40 0.613 ± 0.007 ns/op
从测试结果可以看出,两个方法测试的结果几乎一致,所以我们如果不懂运行优化的话,可能就会错误的认为创建一个对象并不消耗时间。
产生上面的现象是由于JIT编译器会优化掉多余的代码造成的。因为 new Object() 没有在程序的任何地方被使用到,因此这段代码在编译时被消除了,所以导致测试结果跟第一个方法一样。
解决方法1:在程序中返回该对象。
@Benchmark@OutputTimeUnit(TimeUnit.NANOSECONDS)@BenchmarkMode(Mode.AverageTime)public Object pillarsOfCreation() { return new Object();}
解决方法2:使用 Blackhole 。
@Benchmark@OutputTimeUnit(TimeUnit.NANOSECONDS)@BenchmarkMode(Mode.AverageTime)public void blackHole(Blackhole blackhole) { blackhole.consume(new Object());}
那么正确的测试结果如下:
Benchmark Mode Cnt Score Error UnitsBenchMark.blackHole avgt 20 4.126 ± 0.173 ns/opBenchMark.doNothing avgt 20 0.639 ± 0.012 ns/opBenchMark.objectCreation avgt 20 0.635 ± 0.011 ns/opBenchMark.pillarsOfCreation avgt 20 4.061 ± 0.037 ns/op
那么就可以看出性能差异了。
2、常量折叠
参考示例:
@Benchmarkpublic double foldedLog() { int x = 8; return Math.log(x);}
无论执行多少次,基于常量的计算都会返回完全相同的输出。因此,JIT 编译器很有可能会用其结果替换为函数调用,如下:
@Benchmarkpublic double foldedLog() { return 2.0794415416798357;}
这种情况就称为常量折叠。在这种情况下,常量折叠导致 Math.log 没有被调用,因此会导致测试结果错误。
为了防止常量折叠,我们可以将常量封装在一个状态对象中:
@State(Scope.Benchmark)public static class Log { public int x = 8;}@Benchmarkpublic double log(Log input) { return Math.log(input.x);}
运行测试,结果如下:
Benchmark Mode Cnt Score Error UnitsBenchMark.foldedLog thrpt 20 449313097.433 ± 11850214.900 ops/sBenchMark.log thrpt 20 35317997.064 ± 604370.461 ops/s总结:
本文介绍了JMH的使用,更多示例请参考官方:
标签: #javahhhh