使用JMH对Java Object Mapping框架做性能对比

标签: java   jmh   mapping-framework  

JMH概述

JMH 是一个由 OpenJDK/Oracle 里面那群开发了 Java 编译器的大牛们所开发的 Micro Benchmark Framework 。何谓 Micro Benchmark 呢?简单地说就是在 method 层面上的 benchmark,精度可以精确到微秒级。可以看出 JMH 主要使用在当你已经找出了热点函数,而需要对热点函数进行进一步的优化时,就可以使用 JMH 对优化的效果进行定量的分析。

比较典型的使用场景还有:

  • 想定量地知道某个函数需要执行多长时间,以及执行时间和输入 n 的相关性
  • 一个函数有两种不同实现(例如实现 A 使用了 FixedThreadPool,实现 B 使用了 ForkJoinPool),不知道哪种实现性能更好

尽管 JMH 是一个相当不错的 Micro Benchmark Framework,但很无奈的是网上能够找到的文档比较少,而官方也没有提供比较详细的文档,对使用造成了一定的障碍。但是有个好消息是官方的 Code Sample 写得非常浅显易懂,推荐在需要详细了解 JMH 的用法时可以通读一遍——本文则会介绍 JMH 最典型的用法和部分常用选项。

我Fork了一份到github,可以提供大家参考一下,使用gradle构建。jmh-gradle-samples

JMH基本概念

Mode

Mode 表示 JMH 进行 Benchmark 时所使用的模式。通常是测量的维度不同,或是测量的方式不同。目前 JMH 共有四种模式:

  • Throughput: 整体吞吐量,例如“1秒内可以执行多少次调用”。
  • AverageTime: 调用的平均时间,例如“每次调用平均耗时xxx毫秒”。
  • SampleTime: 随机取样,最后输出取样结果的分布,例如“99%的调用在xxx毫秒以内,99.99%的调用在xxx毫秒以内”
  • SingleShotTime: 以上模式都是默认一次 iteration 是 1s,唯有 SingleShotTime只运行一次。往往同时把 warmup 次数设为0,用于测试冷启动时的性能。
  • All: 顾名思义,所有模式,这个在内部测试中常用

Iteration

Iteration 是 JMH 进行测试的最小单位,包含一组invocations。在大部分模式下,一次 iteration 代表的是一秒,JMH 会在这一秒内不断调用需要 benchmark 的方法,然后根据模式对其采样,计算吞吐量,计算平均执行时间等。

Invocation

一次benchmark方法调用

Operation

benchmark方法中,被测量操作的执行。如果被测试的操作在benchmark方法中循环执行,可以使用@OperationsPerInvocation表明循环次数,使测试结果为单次operation的性能。

Warmup

Warmup 是指在实际进行 benchmark 前先进行预热的行为。为什么需要预热?因为 JVM 的 JIT 机制的存在,如果某个函数被调用多次之后,JVM 会尝试将其编译成为机器码从而提高执行速度。所以为了让 benchmark 的结果更加接近真实情况就需要进行预热。

JMH相关注解

现在来解释一下上面例子中使用到的注解,其实很多注解的意义完全可以望文生义 :)

@Benchmark

表示该方法是需要进行 benchmark 的对象,用法和 JUnit 的 @Test 类似。

@Warmup

@Warmup用来配置预热的内容,可用于类或者方法上,越靠近执行方法的地方越准确。一般配置warmup的参数有这些:

  • iterations:预热的次数。
  • time:每次预热的时间。
  • timeUnit:时间单位,默认是s。
  • batchSize:批处理大小,每次操作调用几次方法。

@Measurement

用来控制实际执行的内容,配置的选项和warmup一样。

@BenchmarkMode

Mode 如之前所说,表示 JMH 进行 Benchmark 时所使用的模式。

@BenchmarkMode主要是表示测量的纬度,有以下这些纬度可供选择:

  • Mode.Throughput 吞吐量纬度
  • Mode.AverageTime 平均时间
  • Mode.SampleTime 抽样检测
  • Mode.SingleShotTime 检测一次调用

@State

State 用于声明某个类是一个“状态”,然后接受一个 Scope 参数用来表示该状态的共享范围。因为很多 benchmark 会需要一些表示状态的类,JMH 允许你把这些类以依赖注入的方式注入到 benchmark 函数里。Scope 主要分为两种。

  • Scope.Benchmark 该状态的意思是会在所有的Benchmark的工作线程中共享变量内容。
  • Scope.Group 同一个Group的线程可以享有同样的变量
  • Scope.Thread 每隔线程都享有一份变量的副本,线程之间对于变量的修改不会相互影响。

关于State的用法,官方的 code sample 里有比较好的例子

@OutputTimeUnit

@OutputTimeUnit代表测量的单位,比如秒级别,毫秒级别,微妙级别等等。一般都使用微妙和毫秒级别的稍微多一点。该注解可以用在方法级别和类级别,当用在类级别的时候会被更加精确的方法级别的注解覆盖,原则就是离目标更近的注解更容易生效。

@Fork

用于配置JMH运行时fork的Java进程。使用单独的进程可以避免测试结果之间互相影响。

  • value: fork的进程数量
  • warmups: 每个进程执行Warmup的轮数
  • jvm:进程使用的JVM
  • jvm参数通过以下三个属性,按照从上到下的顺序拼接:
    • jvmArgsPrepend
    • jvmArgs
    • jvmArgsAppend

默认fork的进程数配置在org.openjdk.jmh.runner.Defaults类中:

/**
 * Number of forks in which we measure the workload.
 */
public static final int MEASUREMENT_FORKS = 5;

@Param

在很多情况下,我们需要测试不同的参数的不同结果,但是测试的了逻辑又都是一样的,因此如果我们编写镀铬benchmark的话会造成逻辑的冗余,幸好JMH提供了@Param参数来帮助我们处理这个事情,被@Param注解标示的参数组会一次被benchmark消费到。

@Threads

测试线程的数量,可以配置在方法或者类上,代表执行测试的线程数量。

@Setup

@Setup 方法注解,会在执行 benchmark 之前被执行,正如其名,主要用于初始化

TearDown

@TearDown 方法注解,与@Setup 相对的,会在所有 benchmark 执行结束以后执行,主要用于资源的回收等。

启动选项

解释完了注解,再来看看 JMH 在启动前设置的参数。

Options opt = new OptionsBuilder()
        .include(FirstBenchmark.class.getSimpleName())
        .forks(1)
        .warmupIterations(5)
        .measurementIterations(5)
        .build();

new Runner(opt).run();

include

benchmark 所在的类的名字,注意这里是使用正则表达式对所有类进行匹配的。

fork

进行 fork 的次数。如果 fork 数是2的话,则 JMH 会 fork 出两个进程来进行测试。

warmupIterations

预热的迭代次数。

measurementIterations

实际测量的迭代次数。

运行结果

> Task :JMHSample_03_States.main()
# JMH version: 1.23
# VM version: JDK 1.8.0_181, Java HotSpot(TM) 64-Bit Server VM, 25.181-b13
# VM invoker: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/bin/java
# VM options: -Dfile.encoding=UTF-8 -Duser.country=CN -Duser.language=en -Duser.variant
# Warmup: 5 iterations, 10 s each
# Measurement: 5 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 4 threads, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: org.openjdk.jmh.samples.JMHSample_03_States.measureShared

# Run progress: 0.00% complete, ETA 00:03:20
# Fork: 1 of 1
# Warmup Iteration   1: 40171051.842 ops/s
# Warmup Iteration   2: 40252218.743 ops/s
# Warmup Iteration   3: 40364177.716 ops/s
# Warmup Iteration   4: 40404016.867 ops/s
# Warmup Iteration   5: 40404799.770 ops/s
Iteration   1: 40647891.459 ops/s
Iteration   2: 40493191.378 ops/s
Iteration   3: 40439925.585 ops/s
Iteration   4: 40304980.399 ops/s
Iteration   5: 39981776.259 ops/s


Result "org.openjdk.jmh.samples.JMHSample_03_States.measureShared":
  40373553.016 ±(99.9%) 966832.718 ops/s [Average]
  (min, avg, max) = (39981776.259, 40373553.016, 40647891.459), stdev = 251083.387
  CI (99.9%): [39406720.298, 41340385.734] (assumes normal distribution)


# JMH version: 1.23
# VM version: JDK 1.8.0_181, Java HotSpot(TM) 64-Bit Server VM, 25.181-b13
# VM invoker: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/bin/java
# VM options: -Dfile.encoding=UTF-8 -Duser.country=CN -Duser.language=en -Duser.variant
# Warmup: 5 iterations, 10 s each
# Measurement: 5 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 4 threads, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: org.openjdk.jmh.samples.JMHSample_03_States.measureUnshared

# Run progress: 50.00% complete, ETA 00:01:40
# Fork: 1 of 1
# Warmup Iteration   1: 610056952.435 ops/s
# Warmup Iteration   2: 604463030.904 ops/s
# Warmup Iteration   3: 598467532.966 ops/s
# Warmup Iteration   4: 604482926.268 ops/s
# Warmup Iteration   5: 609670717.845 ops/s
Iteration   1: 595976801.767 ops/s
Iteration   2: 606049671.220 ops/s
Iteration   3: 612153531.508 ops/s
Iteration   4: 540723287.097 ops/s
Iteration   5: 569040967.811 ops/s


Result "org.openjdk.jmh.samples.JMHSample_03_States.measureUnshared":
  584788851.881 ±(99.9%) 114160199.154 ops/s [Average]
  (min, avg, max) = (540723287.097, 584788851.881, 612153531.508), stdev = 29647041.189
  CI (99.9%): [470628652.726, 698949051.035] (assumes normal distribution)


# Run complete. Total time: 00:03:20

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
JMHSample_03_States.measureShared    thrpt    5   40373553.016 ±    966832.718  ops/s
JMHSample_03_States.measureUnshared  thrpt    5  584788851.881 ± 114160199.154  ops/s

启动参数可以配置,但是也有默认值,可以参考类:org.openjdk.jmh.runner.Defaults

配置的优先级

OptionsBuilder > 方法注解 > 类注解

Java对象Mapping框架性能对比

Mapping框架使用的版本,一下都是基于2020-03-23最新的版本:

框架 版本 发布时间
dozer 5.5.1 2014-04
orika 1.5.4 2019-02
mapstruct 1.3.1.Final 2019-09
modelmapper 2.3.6 2019-12

机器:

MacBook Pro (15-inch, 2018)

下面进行两组测试,一组简单代码测试,一组真实项目测试。

代码:https://github.com/amuguelove/java-mapping-frameworks-benchmark

简单测试

Throughput:

框架 Throughput(operations per milliseconds)
dozer 796
mapstruct 184902
modelmapper 1171
orika 2428

AverageTime:

框架 AverageTime(milliseconds per operation)
dozer 0.001
mapstruct 10⁻⁵
modelmapper 0.001
orika 10⁻³

SingleShotTime

框架 SingleShotTime(milliseconds per operation)
dozer 10.871
mapstruct 0.418
modelmapper 5.393
orika 4.362

SampleTime(ms per operation)

框架 P0.90 P0.999 P1.00
dozer 0.001 0.005 1.288
mapstruct 10⁻⁴ 10⁻⁴ 1.270
modelmapper 0.001 0.003 1.239
orika 0.001 0.001 1.307

真实项目测试:

Throughput:

框架 Throughput(operations per milliseconds)
dozer 13
mapstruct 6587
modelmapper 13
orika 355

AverageTime:

框架 AverageTime(milliseconds per operation)
dozer 0.068
mapstruct 10⁻⁴
modelmapper 0.060
orika 0.003

SingleShotTime

框架 SingleShotTime(milliseconds per operation)
dozer 20.794
mapstruct 0.769
modelmapper 20.267
orika 31.194

SampleTime(ms per operation)

框架 P0.90 P0.999 P1.00
dozer 0.061 0.182 2.302
mapstruct 10⁻⁴ 0.002 2.101
modelmapper 0.053 0.169 2.245
orika 0.002 0.023 2.159

综上:mapstruct的性能要好于其他的mapping框架。

「真诚赞赏,手留余香」

请我喝杯咖啡?

使用微信扫描二维码完成支付