【精通性能优化:解锁JMH微基准测试】一基本用法

这篇具有很好参考价值的文章主要介绍了【精通性能优化:解锁JMH微基准测试】一基本用法。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1. 什么是JMH

JMH是Java Micro Benchmark Harness的简写,是专门用于代码微基准测试的工具集。

JMH由实现Java虚拟你的团队开发,现代JVM已经变的越来越智能,在Java文件的编译阶段、类的加载阶段,以及运行阶段都可能进行了不同程度的优化,因此开发者编写的代码未必会像自己所预期的那样具有相同的性能体现,JMH能够让普通开发者能够了解自己所编写的代码运行的情况。

JMH GItHub

1.1 用JMH进行微基准测试

首先我们要将JMH的依赖加入我们的工程之中,这里使用最新的1.36版本

<!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-core -->
<dependency>
  <groupId>org.openjdk.jmh</groupId>
  <artifactId>jmh-core</artifactId>
  <version>1.36</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-generator-annprocess -->
<dependency>
  <groupId>org.openjdk.jmh</groupId>
  <artifactId>jmh-generator-annprocess</artifactId>
  <version>1.36</version>
</dependency>
1. JmhExample01.java
package com.myf.concurrent.wwj2.jmh;

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;
import org.openjdk.jmh.runner.options.TimeValue;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * @author myf
 */
@BenchmarkMode(Mode.SampleTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Thread)
public class JmhExample01 {
    private final static String DATA = "DUMMY DATA";

    private List<String> arrayList;
    private List<String> linkedList;

    @Setup(Level.Invocation)
    public void setUp(){
        this.arrayList = new ArrayList<>();
        this.linkedList = new LinkedList<>();
    }

    @Benchmark
    public List<String> arrayListAdd(){
        this.arrayList.add(DATA);
        return arrayList;
    }

    @Benchmark
    public List<String> linkedListAdd(){
        this.linkedList.add(DATA);
        return linkedList;
    }

    public static void main(String[] args) throws RunnerException {
        final Options options = new OptionsBuilder()
                .include(JmhExample01.class.getSimpleName())
                .forks(1)
                .measurementIterations(10)
                .measurementTime(TimeValue.seconds(1))
                .warmupIterations(10)
                .warmupTime(TimeValue.seconds(1))
                .build();
        new Runner(options).run();
    }
}

上面的程序中,我们使用了一些基本的JMH API,也许目前你还不太了解他们的用法,我们后面会逐一进行讲解。

2. 程序输出JmhExample01.java
# JMH version: 1.36
# VM version: JDK 17.0.2, Java HotSpot(TM) 64-Bit Server VM, 17.0.2+8-LTS-86
# VM invoker: /Library/Java/JavaVirtualMachines/jdk-17.0.2.jdk/Contents/Home/bin/java
# VM options: -javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=56684:/Applications/IntelliJ IDEA.app/Contents/bin -Dfile.encoding=UTF-8
# Blackhole mode: compiler (auto-detected, use -Djmh.blackhole.autoDetect=false to disable)
# Warmup: 10 iterations, 100 ms each
# Measurement: 10 iterations, 100 ms each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: com.myf.concurrent.wwj2.jmh.JmhExample01.arrayListAdd

# Run progress: 0.00% complete, ETA 00:00:04
# Fork: 1 of 1
# Warmup Iteration   1: 63.109 ns/op
# Warmup Iteration   2: 64.557 ns/op
# Warmup Iteration   3: 29.116 ns/op
# Warmup Iteration   4: 23.864 ns/op
# Warmup Iteration   5: 23.078 ns/op
# Warmup Iteration   6: 23.269 ns/op
# Warmup Iteration   7: 24.339 ns/op
# Warmup Iteration   8: 37.417 ns/op
# Warmup Iteration   9: 37.715 ns/op
# Warmup Iteration  10: 33.296 ns/op
Iteration   1: 28.710 ns/op
Iteration   2: 23.029 ns/op
Iteration   3: 23.790 ns/op
Iteration   4: 34.254 ns/op
Iteration   5: 31.454 ns/op
Iteration   6: 30.230 ns/op
Iteration   7: 21.331 ns/op
Iteration   8: 25.501 ns/op
Iteration   9: 29.648 ns/op
Iteration  10: 38.189 ns/op


Result "com.myf.concurrent.wwj2.jmh.JmhExample01.arrayListAdd":
  28.614 ±(99.9%) 8.007 ns/op [Average]
  (min, avg, max) = (21.331, 28.614, 38.189), stdev = 5.296
  CI (99.9%): [20.606, 36.621] (assumes normal distribution)


# JMH version: 1.36
# VM version: JDK 17.0.2, Java HotSpot(TM) 64-Bit Server VM, 17.0.2+8-LTS-86
# VM invoker: /Library/Java/JavaVirtualMachines/jdk-17.0.2.jdk/Contents/Home/bin/java
# VM options: -javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=56684:/Applications/IntelliJ IDEA.app/Contents/bin -Dfile.encoding=UTF-8
# Blackhole mode: compiler (auto-detected, use -Djmh.blackhole.autoDetect=false to disable)
# Warmup: 10 iterations, 100 ms each
# Measurement: 10 iterations, 100 ms each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: com.myf.concurrent.wwj2.jmh.JmhExample01.linkedListAdd

# Run progress: 50.00% complete, ETA 00:00:03
# Fork: 1 of 1
# Warmup Iteration   1: 21.183 ns/op
# Warmup Iteration   2: 15.947 ns/op
# Warmup Iteration   3: 16.283 ns/op
# Warmup Iteration   4: 17.095 ns/op
# Warmup Iteration   5: 16.899 ns/op
# Warmup Iteration   6: 16.618 ns/op
# Warmup Iteration   7: 16.415 ns/op
# Warmup Iteration   8: 16.034 ns/op
# Warmup Iteration   9: 16.170 ns/op
# Warmup Iteration  10: 17.569 ns/op
Iteration   1: 16.021 ns/op
Iteration   2: 16.734 ns/op
Iteration   3: 16.535 ns/op
Iteration   4: 17.296 ns/op
Iteration   5: 21.086 ns/op
Iteration   6: 16.149 ns/op
Iteration   7: 16.183 ns/op
Iteration   8: 16.088 ns/op
Iteration   9: 16.659 ns/op
Iteration  10: 16.116 ns/op


Result "com.myf.concurrent.wwj2.jmh.JmhExample01.linkedListAdd":
  16.887 ±(99.9%) 2.311 ns/op [Average]
  (min, avg, max) = (16.021, 16.887, 21.086), stdev = 1.528
  CI (99.9%): [14.576, 19.197] (assumes normal distribution)


# Run complete. Total time: 00:00:05

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.

NOTE: Current JVM experimentally supports Compiler Blackholes, and they are in use. Please exercise
extra caution when trusting the results, look into the generated code to check the benchmark still
works, and factor in a small probability of new VM bugs. Additionally, while comparisons between
different JVMs are already problematic, the performance difference caused by different Blackhole
modes can be very significant. Please make sure you use the consistent Blackhole mode for comparisons.

Benchmark                   Mode  Cnt   Score   Error  Units
JmhExample01.arrayListAdd   avgt   10  28.614 ± 8.007  ns/op
JmhExample01.linkedListAdd  avgt   10  16.887 ± 2.311  ns/op

我们使用JMH基准测试,输出的信息很多,目前我们先去查看输出的最后两行,我们从这两行的信息可以发现arrayListAdd方法调用平均响应时间是28.614纳秒,误差在8.007纳秒,linkedListAdd方法的调用平均响应时间为16.887纳秒,误差在2.311纳秒,测试结果显示后者的性能是高于前者的。

2.2 JMH的基本用法

2.1 @Benchmark标记基准测试方法

与Junit4中@Test注解标记测试方法一样,JMH对基准测试的方法需要使用@Benchmark注解进行标记。

如果一个类中没有任何基准测试方法,那么对其进行基准测试将会出现异常。

删掉上述代码中两个基准测试注解将会报如下错误:

Exception in thread "main" No benchmarks to run; check the include/exclude regexps.
	at org.openjdk.jmh.runner.Runner.internalRun(Runner.java:258)
	at org.openjdk.jmh.runner.Runner.run(Runner.java:209)
	at com.myf.concurrent.wwj2.jmh.JmhExample02.main(JmhExample02.java:53)
2.2 Warmup以及Measurement

Warmup可以直译为“预热”的意思,Warmup做的就是在基准测试代表正式度量之前,先对其进行预热,使得度量代码的执行是经过了类的早期优化、JVM运行期编译、JIT优化之后的最终状态。

Measurement是真正的度量操作,只有度量的轮次会被计入统计之中。

1. 设置全局的Warmup和Measurement(一)

通过构造Options时设置全局的配置,也可以通过类上的注解进行设置

Options options = new OptionsBuilder()
  .include(JmhExample02.class.getSimpleName())
  .forks(1)
  // 进行迭代测试的次数(10批次不是方法调用10次)
  .measurementIterations(10)
  // 每次迭代需要的时间,默认是10S
  .measurementTime(TimeValue.milliseconds(100))
  // 进行迭代预热的次数
  .warmupIterations(10)
  // 每次迭代需要的时间,默认是10S
  .warmupTime(TimeValue.milliseconds(100))
  .build();
2. 设置全局的Warmup和Measurement(二)

通过注解指定预热和度量的配置

@Measurement(iterations = 10, time = 100, timeUnit = TimeUnit.MILLISECONDS)
@Warmup(iterations = 10, time = 100, timeUnit = TimeUnit.MILLISECONDS)
public class JmhExample01 {
3. 在基准测试方法上设置Warmup和Measurement

注解也可以设置在基准测试方法上,只对当前方法生效。

@Benchmark
@Measurement(iterations = 5, time = 100, timeUnit = TimeUnit.MILLISECONDS)
@Warmup(iterations = 5, time = 100, timeUnit = TimeUnit.MILLISECONDS)
public List<String> arrayListAdd() {
  this.arrayList.add(DATA);
  return arrayList;
}

经测试,当前1.36版本基准测试方法上的注解可以覆盖类上的注解配置,无法覆盖Option中的配置

2.3 详解基准测试相关的输出
# 使用的JMH版本
# JMH version: 1.36
# 下面是JDK的版本信息
# VM version: JDK 17.0.2, Java HotSpot(TM) 64-Bit Server VM, 17.0.2+8-LTS-86
# Java命令的目录
# VM invoker: /Library/Java/JavaVirtualMachines/jdk-17.0.2.jdk/Contents/Home/bin/java
# JVM运行时指定的参数
# VM options: -javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=54283:/Applications/IntelliJ IDEA.app/Contents/bin -Dfile.encoding=UTF-8
# Blackhole mode: compiler (auto-detected, use -Djmh.blackhole.autoDetect=false to disable)
# 热身的批次为10,每一个批次都将会不断地调用arrayListAdd方法,每一个批次的执行时间为100ms
# Warmup: 10 iterations, 100 ms each
# 真正度量的批次为10,这10个批次的调用产生的性能数据才会真正地纳入统计中,同样每一个批次的度量执行的时间也为100ms
# Measurement: 10 iterations, 100 ms each
# 每一个批次的超时时间
# Timeout: 10 min per iteration
# 执行基准测试的线程数量
# Threads: 1 thread, will synchronize iterations
# Benchmark的Mode,这里表明统计的方法是方法调用一次所耗费的单位时间。
# Benchmark mode: Average time, time/op
# 当前测试方法的绝对路径
# Benchmark: com.myf.concurrent.wwj2.jmh.JmhExample01.arrayListAdd

# 执行进度
# Run progress: 0.00% complete, ETA 00:00:04
# Fork: 1 of 1
# 执行十个批次的热身,第一批次调用方法的平均耗时为35.756纳秒,第二批次调用方法的平均耗时为24.443纳秒
# Warmup Iteration   1: 35.756 ns/op
# Warmup Iteration   2: 24.443 ns/op
# Warmup Iteration   3: 28.334 ns/op
# Warmup Iteration   4: 35.592 ns/op
# Warmup Iteration   5: 22.711 ns/op
# Warmup Iteration   6: 25.595 ns/op
# Warmup Iteration   7: 28.407 ns/op
# Warmup Iteration   8: 37.375 ns/op
# Warmup Iteration   9: 39.011 ns/op
# Warmup Iteration  10: 33.058 ns/op
# 执行十个批次的度量
Iteration   1: 26.739 ns/op
Iteration   2: 22.633 ns/op
Iteration   3: 24.204 ns/op
Iteration   4: 20.832 ns/op
Iteration   5: 20.575 ns/op
Iteration   6: 20.881 ns/op
Iteration   7: 21.091 ns/op
Iteration   8: 20.630 ns/op
Iteration   9: 21.314 ns/op
Iteration  10: 22.101 ns/op

# 。。。略略略。。。

# 最终的统计结果
Benchmark                   Mode  Cnt   Score   Error  Units
JmhExample01.arrayListAdd   avgt   10  22.100 ± 3.000  ns/op
JmhExample01.linkedListAdd  avgt   10  16.364 ± 1.325  ns/op
2.4 四大BenchmarkMode

JMH使用@BenchmarkMode这个注解来声明使用哪一种模式来运行,JMH为我们提供了四种运行模式。

JMH允许若干个模式同时存在。

@BenchmarkMode注解可设置在类上也可以设置在基准方法上,即准方法上的设置会覆盖类上的设置,也可以在Option中通过.mode()方法设置,基准测试方法无法覆盖Option中的设置。

1. AverageTime

平均响应时间:输出基准测试方法每次调用所耗费的时间

Benchmark                   Mode  Cnt   Score   Error  Units
JmhExample01.arrayListAdd   avgt   10  22.100 ± 3.000  ns/op
JmhExample01.linkedListAdd  avgt   10  16.364 ± 1.325  ns/op
2. Throughput

方法吞吐量:输出信息表明在单位时间内可以对该方法调用多少次

本次测试我调整了这个为微妙@OutputTimeUnit(TimeUnit.MICROSECONDS)

Benchmark                    Mode  Cnt   Score   Error   Units
JmhExample01.arrayListAdd   thrpt   10  48.007 ± 1.863  ops/us
JmhExample01.linkedListAdd  thrpt   10  62.169 ± 1.439  ops/us
3. SampleTime

时间采样:采用一种抽样的方式来统计基准测试方法的性能结果,他会收集所有的性能数据,并将其分布在不同的区间中。

从输出结果可以发现,对arrayListAdd进行了29951次调用,平均响应时间为0.244微妙,总共有29920的数据点落在了0-50微妙这个区间之中

# 。。。略略略。。。
Result "com.myf.concurrent.wwj2.jmh.JmhExample01.arrayListAdd":
  N = 29951
  mean =      0.244 ±(99.9%) 0.131 us/op

  Histogram, us/op:
    [  0.000,  50.000) = 29920 
    [ 50.000, 100.000) = 15 
    [100.000, 150.000) = 8 
    [150.000, 200.000) = 2 
    [200.000, 250.000) = 0 
    [250.000, 300.000) = 1 
    [300.000, 350.000) = 2 
    [350.000, 400.000) = 1 
    [400.000, 450.000) = 0 
    [450.000, 500.000) = 1 
    [500.000, 550.000) = 0 
    [550.000, 600.000) = 0 
    [600.000, 650.000) = 0 

  Percentiles, us/op:
      p(0.0000) =0 us/op
     p(50.0000) =      0.042 us/op
     p(90.0000) =      0.083 us/op
     p(95.0000) =      0.125 us/op
     p(99.0000) =      0.250 us/op
     p(99.9000) =     51.807 us/op
     p(99.9900) =    364.157 us/op
     p(99.9990) =    679.936 us/op
     p(99.9999) =    679.936 us/op
    p(100.0000) =    679.936 us/op

# 。。。略略略。。。

Benchmark                                           Mode    Cnt    Score   Error  Units
JmhExample01.arrayListAdd                         sample  29951    0.244 ± 0.131  us/op
JmhExample01.arrayListAdd:arrayListAdd·p0.00      sample             ≈ 0          us/op
JmhExample01.arrayListAdd:arrayListAdd·p0.50      sample           0.042          us/op
JmhExample01.arrayListAdd:arrayListAdd·p0.90      sample           0.083          us/op
JmhExample01.arrayListAdd:arrayListAdd·p0.95      sample           0.125          us/op
JmhExample01.arrayListAdd:arrayListAdd·p0.99      sample           0.250          us/op
JmhExample01.arrayListAdd:arrayListAdd·p0.999     sample          51.807          us/op
JmhExample01.arrayListAdd:arrayListAdd·p0.9999    sample         364.157          us/op
JmhExample01.arrayListAdd:arrayListAdd·p1.00      sample         679.936          us/op
JmhExample01.linkedListAdd                        sample  26853    0.023 ± 0.003  us/op
JmhExample01.linkedListAdd:linkedListAdd·p0.00    sample             ≈ 0          us/op
JmhExample01.linkedListAdd:linkedListAdd·p0.50    sample             ≈ 0          us/op
JmhExample01.linkedListAdd:linkedListAdd·p0.90    sample           0.042          us/op
JmhExample01.linkedListAdd:linkedListAdd·p0.95    sample           0.042          us/op
JmhExample01.linkedListAdd:linkedListAdd·p0.99    sample           0.042          us/op
JmhExample01.linkedListAdd:linkedListAdd·p0.999   sample           0.125          us/op
JmhExample01.linkedListAdd:linkedListAdd·p0.9999  sample          11.186          us/op
JmhExample01.linkedListAdd:linkedListAdd·p1.00    sample          17.312          us/op
4. SingleShotTime(没太懂)

冷测试:无论是Warmup还是Measurement在每一个批次中基准测试方法只会被执行一次,一般我们会讲Warmup的批次设置为0。

# 。。。略略略。。。

Result "com.myf.concurrent.wwj2.jmh.JmhExample01.arrayListAdd":
  N = 10
  mean =      1.421 ±(99.9%) 0.986 us/op

  Histogram, us/op:
    [0.000, 0.250) = 0 
    [0.250, 0.500) = 0 
    [0.500, 0.750) = 0 
    [0.750, 1.000) = 4 
    [1.000, 1.250) = 2 
    [1.250, 1.500) = 1 
    [1.500, 1.750) = 0 
    [1.750, 2.000) = 1 
    [2.000, 2.250) = 0 
    [2.250, 2.500) = 1 
    [2.500, 2.750) = 1 

  Percentiles, us/op:
      p(0.0000) =      0.875 us/op
     p(50.0000) =      1.125 us/op
     p(90.0000) =      2.638 us/op
     p(95.0000) =      2.667 us/op
     p(99.0000) =      2.667 us/op
     p(99.9000) =      2.667 us/op
     p(99.9900) =      2.667 us/op
     p(99.9990) =      2.667 us/op
     p(99.9999) =      2.667 us/op
    p(100.0000) =      2.667 us/op

# 。。。略略略。。。

Benchmark                   Mode  Cnt  Score   Error  Units
JmhExample01.arrayListAdd     ss   10  1.421 ± 0.986  us/op
JmhExample01.linkedListAdd    ss   10  1.817 ± 0.904  us/op
5. 多Mode以及All

可以设置多个模式的方法运行基准测试方法。

@BenchmarkMode({Mode.AverageTime, Mode.Throughput})
Benchmark                    Mode  Cnt   Score   Error   Units
JmhExample01.arrayListAdd   thrpt   10  44.052 ± 6.503  ops/us
JmhExample01.linkedListAdd  thrpt   10  62.376 ± 1.905  ops/us
JmhExample01.arrayListAdd    avgt   10   0.027 ± 0.016   us/op
JmhExample01.linkedListAdd   avgt   10   0.017 ± 0.002   us/op

甚至可以设置全部Mode

@BenchmarkMode(Mode.All)
Benchmark                                           Mode    Cnt   Score   Error   Units
JmhExample01.arrayListAdd                          thrpt     10  45.572 ± 2.751  ops/us
JmhExample01.linkedListAdd                         thrpt     10  62.004 ± 2.739  ops/us
JmhExample01.arrayListAdd                           avgt     10   0.022 ± 0.001   us/op
JmhExample01.linkedListAdd                          avgt     10   0.016 ± 0.001   us/op
JmhExample01.arrayListAdd                         sample  24399   0.032 ± 0.014   us/op
JmhExample01.arrayListAdd:arrayListAdd·p0.00      sample            ≈ 0           us/op
JmhExample01.arrayListAdd:arrayListAdd·p0.50      sample          0.041           us/op
JmhExample01.arrayListAdd:arrayListAdd·p0.90      sample          0.042           us/op
JmhExample01.arrayListAdd:arrayListAdd·p0.95      sample          0.042           us/op
JmhExample01.arrayListAdd:arrayListAdd·p0.99      sample          0.125           us/op
JmhExample01.arrayListAdd:arrayListAdd·p0.999     sample          0.817           us/op
JmhExample01.arrayListAdd:arrayListAdd·p0.9999    sample         13.082           us/op
JmhExample01.arrayListAdd:arrayListAdd·p1.00      sample         99.328           us/op
JmhExample01.linkedListAdd                        sample  26468   0.023 ± 0.001   us/op
JmhExample01.linkedListAdd:linkedListAdd·p0.00    sample            ≈ 0           us/op
JmhExample01.linkedListAdd:linkedListAdd·p0.50    sample          0.041           us/op
JmhExample01.linkedListAdd:linkedListAdd·p0.90    sample          0.042           us/op
JmhExample01.linkedListAdd:linkedListAdd·p0.95    sample          0.042           us/op
JmhExample01.linkedListAdd:linkedListAdd·p0.99    sample          0.042           us/op
JmhExample01.linkedListAdd:linkedListAdd·p0.999   sample          0.272           us/op
JmhExample01.linkedListAdd:linkedListAdd·p0.9999  sample          3.668           us/op
JmhExample01.linkedListAdd:linkedListAdd·p1.00    sample          6.288           us/op
JmhExample01.arrayListAdd                             ss     10   0.583 ± 0.165   us/op
JmhExample01.linkedListAdd                            ss     10   1.100 ± 1.036   us/op
2.5 OutputTimeUnit

OutputTimeUnit提供了统计结果输出时的时间单位

2.6 三大State的使用

State用于标记状态对象,状态对象封装了基准测试处理的对象的状态,状态对象的作用域(scope)定义了它在工作线程之间共享的状态。

状态对象通常作为参数注入Benchmark方法,JMH负责状态对象的实例化和共享。状态对象也可以被注入到其他状态对象的Setup和TearDown方法中,以或得阶段初始化。State-s之间的依赖关系图应该是有向无环图。

State对象可以被继承:您可以将State放在一个超类上,并使用子类作为状态。

1. Thread独享State

每一个运行基准测试方法的线程都会持有一个独立的对象实例,该实例既有可能是作为基准测试方法参数传入的,也有可能是运行基准测试方法所在的宿主class(前面测试用例我们在类上就加了相关配置),将State设置为Scope.Thread一般主要是针对非线程安全的类。

package com.myf.concurrent.wwj2.jmh;

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;

import java.util.concurrent.TimeUnit;

/**
 * @author myf
 */
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Fork(1)
@Warmup(iterations = 5, time = 100, timeUnit = TimeUnit.MILLISECONDS)
@Measurement(iterations = 10, time = 100, timeUnit = TimeUnit.MILLISECONDS)
// 设置5个线程运行基准测试方法
@Threads(5)
public class JmhExample06 {

    // 5个运行线程,每一个线程都会持有一个Test实例
    @State(Scope.Thread)
    public static class Test{
        public Test() {
            System.out.println("create Test instance");
        }

        public void method(){

        }
    }

    // JMH将状态对象Test作为参数引入
    @Benchmark
    public void test(Test test){
        test.method();
    }

    public static void main(String[] args) throws RunnerException {
        final Options options = new OptionsBuilder()
                .include(JmhExample06.class.getSimpleName())
                .build();
        new Runner(options).run();
    }

}

运行上面的程序,我们会看到"create Test instance"字样出现了5次。即每一个线程都会持有单独的Test实例。

# 。。。略略略。。。
# Benchmark: com.myf.concurrent.wwj2.jmh.JmhExample06.test

# Run progress: 0.00% complete, ETA 00:00:01
# Fork: 1 of 1
# Warmup Iteration   1: create Test instance
create Test instance
create Test instance
create Test instance
create Test instance
599.513 ±(99.9%) 1401.639 ns/op
# Warmup Iteration   2: 7.567 ±(99.9%) 4.938 ns/op
# Warmup Iteration   3: 3.152 ±(99.9%) 2.982 ns/op
# Warmup Iteration   4: 1.401 ±(99.9%) 0.911 ns/op
# Warmup Iteration   5: 1.688 ±(99.9%) 1.052 ns/op

# 。。。略略略。。。
2. Thread共享State

我们需要测试多线程的情况下某个类被不同线程操作时的性能,JMH提供了对县城共享的一种状态Scope.Benchmark

package com.myf.concurrent.wwj2.jmh;

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;

import java.util.concurrent.TimeUnit;

/**
 * @author myf
 */
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Fork(1)
@Warmup(iterations = 5, time = 100, timeUnit = TimeUnit.MILLISECONDS)
@Measurement(iterations = 10, time = 100, timeUnit = TimeUnit.MILLISECONDS)
// 设置5个线程运行基准测试方法
@Threads(5)
public class JmhExample07 {

    // Test实例将会被多个线程共享
    @State(Scope.Benchmark)
    public static class Test{
        public Test() {
            System.out.println("create Test instance");
        }

        public void method(){

        }
    }

    // JMH将状态对象Test作为参数引入
    @Benchmark
    public void test(Test test){
        test.method();
    }

    public static void main(String[] args) throws RunnerException {
        final Options options = new OptionsBuilder()
                .include(JmhExample07.class.getSimpleName())
                .build();
        new Runner(options).run();
    }

}

运行上面的程序,我们会看到"create Test instance"字样出现了1次。即Test实例将会被多个线程共享。

# 。。。略略略。。。

# Benchmark: com.myf.concurrent.wwj2.jmh.JmhExample07.test

# Run progress: 0.00% complete, ETA 00:00:01
# Fork: 1 of 1
# Warmup Iteration   1: create Test instance
6.185 ±(99.9%) 11.608 ns/op
# Warmup Iteration   2: 0.833 ±(99.9%) 0.245 ns/op
# Warmup Iteration   3: 1.544 ±(99.9%) 0.418 ns/op
# Warmup Iteration   4: 1.573 ±(99.9%) 0.917 ns/op

# 。。。略略略。。。
3. 线程组共享State

前面的实例中,我们所编写的基准测试方法都会被JMH框架根据方法名的字典顺序排序后逐个调用执行,因此不存在两个方法同时运行的情况。

如果我们想测试如下问题,第一,在多线程情况下的单个实例;第二允许一个以上的基准测试方法并行地运行。

这时候我们就要用到Scope.Group

package com.myf.concurrent.wwj2.jmh;

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;

import java.util.concurrent.TimeUnit;

/**
 * @author myf
 */
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Fork(1)
@Warmup(iterations = 1, time = 100, timeUnit = TimeUnit.MILLISECONDS)
@Measurement(iterations = 5, time = 100, timeUnit = TimeUnit.MILLISECONDS)
public class JmhExample08 {

    // Test实例将会被设置为线程组共享的
    @State(Scope.Group)
    public static class Test{
        public Test() {
            System.out.println("create Test instance");
        }

        public void write(){
            System.out.println("write");
        }

        public void read(){
            System.out.println("read");
        }
    }

    // 在线程组test中,有三个线程将不断对Test实例的read方法进行调用
    @GroupThreads(3)
    @Group("test")
    @Benchmark
    public void testRead(Test test){
        test.read();
    }

    // 在线程组test中,有三个线程将不断对Test实例的write方法进行调用
    @GroupThreads(3)
    @Group("test")
    @Benchmark
    public void testWrite(Test test){
        test.write();
    }

    public static void main(String[] args) throws RunnerException {
        final Options options = new OptionsBuilder()
                .include(JmhExample08.class.getSimpleName())
                .build();
        new Runner(options).run();
    }

}

执行上述代码,我们会发现read和write分别交替输出。

# 。。。略略略。。。
# Timeout: 10 min per iteration
# 总共6个线程会执行基准测试方法,这6个线程在同一个Group中
# Threads: 6 threads (1 group; 3x "testRead", 3x "testWrite" in each group), will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: com.myf.concurrent.wwj2.jmh.JmhExample08.test

# Run progress: 0.00% complete, ETA 00:00:00
# Fork: 1 of 1
# Warmup Iteration   1: create Test instance

# 。。。略略略。。。
# read和write分别交替输出
read
read
read
read
write
write
write
read
read
read
read
# 。。。略略略。。。

Benchmark                    Mode  Cnt      Score        Error  Units
JmhExample08.test            avgt    5  84965.982 ±  71625.520  ns/op
JmhExample08.test:testRead   avgt    5  92158.539 ± 147051.981  ns/op
JmhExample08.test:testWrite  avgt    5  77773.425 ± 111967.836  ns/op
2.7 @Param的妙用

假设你在编写代码的过程中需要用到一个Map容器,第一,需要保证使用过程中线程的安全性,第二,该容器需要有比较好的性能,比如,执行put方法最快,执行get方法最快等。作为Java程序员,JDK可供我们选择的方案其实有不少,比如ConcurrentHashMap、Hashtable、ConcurrentSkipListMap以及SynchronizedMap等,虽然它们都能够保证在多线程操作下的数据一致性,但是各自的性能表现又是怎样的呢?这就需要我们对其进行微基准测试(我们的测试相对来说比较片面,只在多线程的情况下对其进行put操作,也就是说并未涉及读取以及删除的操作)。

1. 对比ConcurrentHashMap和SynchronizedMap的性能

根据前面所学知识我们可以轻松写出满足该对比需求的基准测试。

package com.myf.concurrent.wwj2.jmh;

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;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

@BenchmarkMode(Mode.AverageTime)
@Fork(1)
@Warmup(iterations = 5, time = 100, timeUnit = TimeUnit.MILLISECONDS)
@Measurement(iterations = 10, time = 100, timeUnit = TimeUnit.MILLISECONDS)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
// 5个线程同事对共享资源进行操作
@Threads(5)
// 将状态对象JmhExample09设置为线程之间共享资源
@State(Scope.Benchmark)
public class JmhExample09 {

    private Map<Long, Long> concurrentMap;
    private Map<Long, Long> synchnoizedMap;

    @Setup
    public void setUp(){
        concurrentMap = new ConcurrentHashMap<>();
        synchnoizedMap = Collections.synchronizedMap(new HashMap<>());
    }

    @Benchmark
    public void testConcurrentMap(){
        this.concurrentMap.put(System.nanoTime(), System.nanoTime());
    }

    @Benchmark
    public void testSynchnoizedMap(){
        this.synchnoizedMap.put(System.nanoTime(), System.nanoTime());
    }

    public static void main(String[] args) throws RunnerException {
        final Options options = new OptionsBuilder()
                .include(JmhExample09.class.getSimpleName())
                .build();
        new Runner(options).run();
    }
}

这里设置测试方法本身为Thread共享State,用五个线程进行测试,输出结果如下:

Benchmark                        Mode  Cnt      Score       Error  Units
JmhExample09.testConcurrentMap   avgt   10   1630.001 ±  1260.341  ns/op
JmhExample09.testSynchnoizedMap  avgt   10  13360.557 ± 38052.348  ns/op

从结果我们可以看出,ConcurrentHashMap的写性能要优于synchronizedMap。

2. 使用@Param

Java提供的具备线程安全的Map接口实现并非只有ConcurrentHashMap和SynchronizedMap,同样,ConcurrentSkipListMap和Hashtable也可供我们选择,如果我们要对其进行测试,那么这里需要再增加两个不同类型的Map和两个针对这两个Map实现的基准测试方法。但是很显然,这种方式存在大量的代码冗余,因此JMH为我们提供了一个@Param的注解,它使得参数可配置,也就是说一个参数在每一次的基准测试时都会有不同的值与之对应。

package com.myf.concurrent.wwj2.jmh;

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;

import java.util.Collections;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.TimeUnit;

@BenchmarkMode(Mode.AverageTime)
@Fork(1)
@Warmup(iterations = 5, time = 100, timeUnit = TimeUnit.MILLISECONDS)
@Measurement(iterations = 10, time = 100, timeUnit = TimeUnit.MILLISECONDS)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
// 5个线程同事对共享资源进行操作
@Threads(5)
// 将状态对象JmhExample09设置为线程之间共享资源
@State(Scope.Benchmark)
public class JmhExample10 {

    @Param({"1", "2", "3", "4"})
    private int type;
    private Map<Long, Long> map;

    @Setup
    public void setUp() {
        switch (type) {
            case 1 -> this.map = new ConcurrentHashMap<>();
            case 2 -> this.map = new ConcurrentSkipListMap<>();
            case 3 -> this.map = new Hashtable<>();
            case 4 -> this.map = Collections.synchronizedMap(new HashMap<>());
            default -> throw new IllegalArgumentException("Illegal map type.");
        }
    }

    @Benchmark
    public void testConcurrentMap() {
        this.map.put(System.nanoTime(), System.nanoTime());
    }

    public static void main(String[] args) throws RunnerException {
        final Options options = new OptionsBuilder()
                .include(JmhExample10.class.getSimpleName())
                .build();
        new Runner(options).run();
    }
}

引进了@Param对变量的可配置化,因此我们只需要写一个基准测试方法即可,JMH会根据@Param所提供的参数值,对test方法分别进行基准测试的运行与统计,这样我们就不需要为每一个map容器都写一个基准测试方法了。

Benchmark                       (type)  Mode  Cnt     Score      Error  Units
JmhExample10.testConcurrentMap       1  avgt   10  1905.870 ± 1855.904  ns/op
JmhExample10.testConcurrentMap       2  avgt   10   918.250 ±  267.272  ns/op
JmhExample10.testConcurrentMap       3  avgt   10  2333.470 ±  689.124  ns/op
JmhExample10.testConcurrentMap       4  avgt   10  4950.044 ± 3697.307  ns/op

运行上面的基准测试,我们会发现输出结果中多了type这样一列信息。

2.8 JMH测试套件(Fixture)
1. Setup和TearDown

JMH提供了两个注解@Setup和@TearDown用于套件测试。

@Setup会在每一个基准测试方法执行前被调用,通常用于资源的初始化。

@TearDown会在基准测试方法被执行之后被调用,通常用于资源的回收清理工作。

默认情况下@Setup和@TearDown会在一个基准方法的所有批次执行前后分别执行

这里我们只需要对JmhExample10,做出以下调整,观察变化

@Setup
public void setUp() {
  System.out.println("setUp执行了" + Thread.currentThread().getName());
  switch (type) {
    case 1 -> this.map = new ConcurrentHashMap<>();
    case 2 -> this.map = new ConcurrentSkipListMap<>();
    case 3 -> this.map = new Hashtable<>();
    case 4 -> this.map = Collections.synchronizedMap(new HashMap<>());
    default -> throw new IllegalArgumentException("Illegal map type.");
  }
}

@TearDown
public void tearDown() {
  System.out.println("tearDown执行了" + Thread.currentThread().getName());
}

我们会发现每一个基准测试方法的输出发生了如下变化

# Run progress: 0.00% complete, ETA 00:00:06
# Fork: 1 of 1
# Warmup Iteration   1: setUp执行了com.myf.concurrent.wwj2.jmh.JmhExample10.testConcurrentMap-jmh-worker-1
1825.038 ±(99.9%) 2807.819 ns/op
# Warmup Iteration   2: 1423.850 ±(99.9%) 1637.913 ns/op
# Warmup Iteration   3: 1267.709 ±(99.9%) 1272.636 ns/op
# Warmup Iteration   4: 1475.872 ±(99.9%) 2460.539 ns/op
# Warmup Iteration   5: 896.906 ±(99.9%) 737.925 ns/op
Iteration   1: 1433.655 ±(99.9%) 1074.316 ns/op
Iteration   2: 2235.900 ±(99.9%) 3502.619 ns/op
Iteration   3: 2860.355 ±(99.9%) 4995.927 ns/op
Iteration   4: 964.292 ±(99.9%) 1568.305 ns/op
Iteration   5: 3285.678 ±(99.9%) 17619.895 ns/op
Iteration   6: 8562.194 ±(99.9%) 66921.642 ns/op
Iteration   7: 1234.296 ±(99.9%) 1633.239 ns/op
Iteration   8: 5399.636 ±(99.9%) 16028.597 ns/op
Iteration   9: 4187.621 ±(99.9%) 7297.895 ns/op
Iteration  10: tearDown执行了com.myf.concurrent.wwj2.jmh.JmhExample10.testConcurrentMap-jmh-worker-4
3684.771 ±(99.9%) 6960.884 ns/op
2. Level

如果需要再每一个批次或者每一次基准测试方法调用执行的前后执行对应的套件方法,则需要对@Setup和@TearDown进行简单的配置。

  • Trial Setup和TearDown默认的配置,该套件方法会在每一个基准测试方法的所有批次执行的前后被执行。

    @Setup(Level.Trial)
    
  • Iteration:由于我们可以设置Warmup和Measurement,因此每一个基准测试方法都会被执行若干个批次,如果想要在每一个基准测试批次执行的前后调用套件方法,则可以将Level设置为Iteration。

    Warmup和Measurement设置的10批次不是方法调用10次

    @Setup(Level.Iteration)
    
  • Invocation:将Level设置为Invocation意味着在每一个批次的度量过程中,每一次对基准方法的调用前后都会执行套件方法。

    Warmup和Measurement设置的10批次不是方法调用10次

    @Setup(Level.Invocation)
    

    执行结果

    setUp执行了com.myf.concurrent.wwj2.jmh.JmhExample10.testConcurrentMap-jmh-worker-5
    setUp执行了com.myf.concurrent.wwj2.jmh.JmhExample10.testConcurrentMap-jmh-worker-5
    setUp执行了com.myf.concurrent.wwj2.jmh.JmhExample10.testConcurrentMap-jmh-worker-5
    setUp执行了com.myf.concurrent.wwj2.jmh.JmhExample10.testConcurrentMap-jmh-worker-5
    setUp执行了com.myf.concurrent.wwj2.jmh.JmhExample10.testConcurrentMap-jmh-worker-5
    setUp执行了com.myf.concurrent.wwj2.jmh.JmhExample10.testConcurrentMap-jmh-worker-5
    setUp执行了com.myf.concurrent.wwj2.jmh.JmhExample10.testConcurrentMap-jmh-worker-5
    setUp执行了com.myf.concurrent.wwj2.jmh.JmhExample10.testConcurrentMap-jmh-worker-5
    tearDown执行了com.myf.concurrent.wwj2.jmh.JmhExample10.testConcurrentMap-jmh-worker-2
    3740.522 ±(99.9%) 2590.521 ns/op
    

需要注意的是,套件方法的执行也会产生CPU时间的消耗,但是JMH并不会将这部分时间纳入基准方法的统计之中,这一点更进一步地说明了JMH的严谨之处。文章来源地址https://www.toymoban.com/news/detail-659174.html

到了这里,关于【精通性能优化:解锁JMH微基准测试】一基本用法的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Java--JMH--性能测试--测试软件运行效率/时间--StopWatch

    写在前面: 很多时候想要测试代码运行时间,或者比较2个运行的效率。 最简单的方法就是Sytem.currentTimeMillis记录2开始和结束时间来算 但是Java 代码越执行越快,放在后面的方法会有优势,这个原因受留个眼,以后研究。大概有受类加载,缓存预热, jit 编译优化等原因。 需要

    2024年01月21日
    浏览(44)
  • 【ceph相关】ceph基准性能测试工具

    参考文档:RedHat-Ceph性能基准 本篇主要介绍几种ceph原生基准性能测试工具以及各自对应使用方法 不同于fio、vdbench等上层应用接口测试工具,ceph提供了一些自带的基准性能测试工具,用于测试rados、rbd等底层存储基准性能,可以比对底层基准性能和上层应用基准性能,确定潜

    2023年04月08日
    浏览(24)
  • 【测试开发】单元测试、基准测试和性能分析(以 Go testing 为例)

    你写不出 bug-free 的代码。 你认为自己写出了 bug-free 的代码,但它在你意想不到的地方出错了。 你觉得自己写出了永不出错的代码,但它的性能十分糟糕。 “测试左移”距离真正落地或许还有不短的距离,但在开发过程中注重自己的代码质量,至少养成 写单测 的习惯还是很

    2024年02月04日
    浏览(36)
  • Hibench对大数据平台CDH/HDP基准性能测试

    1.1、源码/包:https://github.com/Intel-bigdata/HiBench 部署方法: https://github.com/Intel-bigdata/HiBench/blob/master/docs/build-hibench.md 注意:hibench执行需hadoop客户端jar包环境 如何使用HiBench进行基准测试说明: https://cloud.tencent.com/developer/article/1158310 主要介绍下bin执行文件目录和conf配置文件目录

    2024年02月05日
    浏览(35)
  • 服务器基准测试实践:SysBench的搭建与基本使用

    🏆作者简介,黑夜开发者,CSDN领军人物,全栈领域优质创作者✌,CSDN博客专家,阿里云社区专家博主,2023年6月CSDN上海赛道top4。 🏆数年电商行业从业经验, AWS/阿里云资深使用用户 ,历任核心研发工程师,项目技术负责人。 🎉欢迎 👍点赞✍评论⭐收藏 如何更加全面了

    2024年02月09日
    浏览(28)
  • POC CPU 单核性能基准测试 Linux bash 下的简单测试用例 bc计算兀

    通过耗时简单评估CPU单核性能

    2024年02月16日
    浏览(35)
  • 解锁MySQL性能瓶颈!超实用的10种优化方法大揭秘

    💡一个热爱分享高性能服务器后台开发知识的博主,目标是通过理论与代码实践的结合,让世界上看似难以掌握的技术变得易于理解与掌握。技能涵盖了多个领域,包括C/C++、Linux、Nginx、MySQL、Redis、fastdfs、kafka、Docker、TCP/IP、协程、DPDK等。 👉 🎖️ CSDN实力新星,CSDN博客专

    2024年02月03日
    浏览(35)
  • 【计算机架构】计算 CPU 动态功耗 | 集成电路成本 | SPEC 基准测试 | Amdahl 定律 | MIPS 性能指标

           本篇博客全站热榜排名: 8 📜 本章目录: 0x00 动态功耗(Dynamic Power) 0x01 集成电路成本(Integrated Circuit Cost) 0x02 基准测试(SPEC CPU Benchmark) 0x03 SPEC功率基准测试(SPEC Power Benchmark) 0x04 Pitfall:Amdahl’s 定律 0x05 Pitfall:将 MIPS 作为性能指标 0x00 动态功耗(Dynamic P

    2024年02月03日
    浏览(31)
  • 【go从入门到精通】go基本类型和运算符用法

                 大家好,这是我给大家准备的新的一期专栏,专门讲golang,从入门到精通各种框架和中间件,工具类库,希望对go有兴趣的同学可以订阅此专栏。 ---------------------------------------------------------------------------------------------------------------------------------              

    2024年03月08日
    浏览(32)
  • 辅助性能优化——长安链性能分析工具原理及用法

    如何提升区块链系统性能是很多开发者都会关注的事,但是有些对区块链并非十分熟悉的开发者可能会感到没有头绪。长安链提供了性能分析工具帮助开发者梳理系统耗时,优化系统性能。下面对长安链性能分析工具原理及使用进行介绍。 time_counter.sh是长安链性能分析工具,

    2024年02月13日
    浏览(40)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包