您的位置 首页 java

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

JMH简介

JMH(Java Microbenchmark Harness)是用于代码微基准测试的工具套件,主要是基于方法层面的基准测试,精度可以达到纳秒级。

JMH 比较典型的应用场景如下:

  1. 想准确地知道某个方法需要执行多长时间,以及执行时间和输入之间的相关性
  2. 对比接口不同实现在给定条件下的吞吐量
  3. 查看多少百分比的请求在多长时间内完成

下面我们以 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/op
Iteration   2: 2651.981 ±(99.9%) 150.255 ns/op
Iteration   3: 2668.217 ±(99.9%) 176.983 ns/op
Iteration   4: 2554.293 ±(99.9%) 181.387 ns/op
Iteration   5: 2616.313 ±(99.9%) 149.811 ns/op


Result "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  Units
JSONSerializerTest.testFastJson       10  avgt    5   2653.306 ±  312.730  ns/op
JSONSerializerTest.testFastJson       50  avgt    5  13242.378 ±  479.027  ns/op
JSONSerializerTest.testFastJson      100  avgt    5  25789.098 ± 1017.915  ns/op
JSONSerializerTest.testJackson        10  avgt    5   3801.418 ±  476.998  ns/op
JSONSerializerTest.testJackson        50  avgt    5  18727.014 ±  531.329  ns/op
JSONSerializerTest.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作为参数进行使用。 包括以下 时间单位

  • NANOSECONDS
  • MICROSECONDS
  • MILLISECONDS
  • SECONDS
  • MINUTES
  • HOURS
  • DAYS

3、@State

JMH 提供了可以重用状态对象的不同“范围”。状态范围在@State 注释的参数中指定。在上面的例子中,选择的范围是 Scope. Benchmark

Scope 类包含下列范围的常数:

Thread

运行基准测试的每个线程都将创建自己的状态对象实例。

Group

运行基准测试的每个线程组将创建自己的状态对象实例。

Benchmark

运行基准测试的所有线程共享相同的状态对象。

4、@Warmup

预热所需要配置的一些基本测试参数,可用于类或者方法上。一般前几次进行 程序测试 的时候都会比较慢,所以要让程序进行几轮预热,保证测试的准确性。参数如下所示:

  1. iterations:预热的次数
  2. time:每次预热的时间
  3. timeUnit:时间的单位,默认秒
  4. 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  Units
BenchMark.doNothing       avgt   40  0.609 ± 0.006  ns/op
BenchMark.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  Units
BenchMark.blackHole          avgt   20  4.126 ± 0.173  ns/op
BenchMark.doNothing          avgt   20  0.639 ± 0.012  ns/op
BenchMark.objectCreation     avgt   20  0.635 ± 0.011  ns/op
BenchMark.pillarsOfCreation  avgt   20  4.061 ± 0.037  ns/op  

那么就可以看出性能差异了。

2、常量折叠

参考示例:

 @Benchmark
public double foldedLog() {
    int x = 8;
    return Math.log(x);
}  

无论执行多少次,基于常量的计算都会返回完全相同的输出。 因此,JIT 编译器很有可能会用其结果替换为函数调用,如下:

 @Benchmark
public double foldedLog() {
    return 2.0794415416798357;
}  

这种情况就称为常量折叠 。在这种情况下,常量折叠导致 Math.log 没有被 调用,因此会导致测试结果错误。

为了防止常量折叠,我们可以将常量封装在一个状态对象中:

 @State(Scope.Benchmark)
public static class Log {
    public int x = 8;
}

@Benchmark
public double log(Log input) {
     return Math.log(input.x);
}  

运行测试,结果如下:

 Benchmark             Mode  Cnt          Score          Error  Units
BenchMark.foldedLog  thrpt   20  449313097.433 ± 11850214.900  ops/s
BenchMark.log        thrpt   20   35317997.064 ±   604370.461  ops/s  

总结:

本文介绍了JMH的使用,更多示例请参考官方:

文章来源:智云一二三科技

文章标题:Java使用JMH对FastJson和Jackson序列化操作进行基准测试

文章地址:https://www.zhihuclub.com/184035.shtml

关于作者: 智云科技

热门文章

网站地图