龙空技术网

Java使用JMH对FastJson和Jackson序列化操作进行基准测试

迷路的架构师 121

前言:

当前各位老铁们对“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