jmh 微基准测试框架

1. jmh简介

JMH是Java Microbenchmark Harness的简称(微基准测试框架),一个针对Java做基准测试的工具,是由开发JVM那群人开发的。想准确的对一段代码做基准性能测试并不容易,因为JVM层面在编译期、运行时对代码做了很多的优化,但是当代码块处于整个系统运行时这些优化不一定会生效,从而产生错误的基准测试结果,而这个问题就是JMH要解决的。

Java的基准测试需要注意的几个点:

  • 测试前需要预热

  • 防止无用代码进入测试方法中

  • 并发测试

  • 测试结果呈现

使用场景

  1. 定量分析某个热点函数的额优化效果
  1. 想定量地知道某个函数需要执行多长事件,以及执行时间和输入变量的相关性
  1. 对比一个函数的多种实现方式

Benchmark 基本概念

Benchmark State

有时候我们在做基准测试的时候会需要使用一些变量、字段,@State注解是用来配置这些变量的声明周期,@State注解可以放在类上,然后在基准测试方法中可以通过参数的方式把该类对象作为参数使用

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

@State支持的生命周期类型:

  • Benchmark: 整个基准测试的生命周期,多个线程共用同一份实例对象。该类内部的@Setup@TearDown注解的方法可能会被任意一个线程执行,但只会执行一次
  • Group: 每一个Group内部共享同一个实例,需要配合@Group@GroupThread使用。该类内部的@Setup@TearDown注解的方法可能会被该Group内部的任意一个线程执行,但是只会执行一次
  • Thread: 每个线程的实例都是不同的、唯一的。该类内部的@Setup@TearDown注解的方法只会被当前线程执行,而且只会执行一次

@State标识的类必须满足以下两个要求:

  1. 类必须是public

  2. 必须有午餐构造函数

State Object @Setup @TearDown

@Scope注解标识的类上的方法上可以添加@Setup@TearDown注解。

  • @Setup: 用来标识在Benchmark方法使用State对象之前需要执行的操作
  • @TearDown: 用来标识在Benchmark方法之后需要对State对象执行的操作

@Setup@TearDown支持设置Level级别,Level有三个值:

  • Trial: 每次Benchmark前/后执行一次,每次Benchmark会包含多轮Iteration
  • Iteration: 每轮执行前/后执行一次
  • Invocation:每次调用测试的方法前/后都执行一次,这个执行频率会很高,一般用不上

Fork

@Fork注解用来设置启动的JVM进程数量,多个进程是串行的方式启动的,多个进程可以减少偶发因素对测试结果的影响

Thread

@Thread用来配置执行测试启动的线程数量

Warmup

@Warmup用来配置预热的事件,如下所示配置预热5轮,每轮1 second,也就是说总共预热5s左右,在这5s内不停的循环调用测试方法,但是预热时的数据不作为测试结果参考

Benchmark Mode

JMH benchmark支持如下几种测试模式:

  • Trhroughput: 吞吐量,测试美妙可以执行操作的次数
  • Average Time: 平均耗时,测试单次操作的平均耗时
  • Sample Time: 采样耗时,测试单次操作的耗时,包括最大、最小耗时,以及百分位耗时等
  • Single Shot Time: 只计算一次的耗时,一般用来测试冷启动的性能(不设置JVM预热)
  • ALL: 测试上面的所有指标

默认的Benchmark Mode是Throughput,可以通过注解的方式设置BenchmarkMode,注解支持放在类或方法上。

2. 代码工程

实验目的:

  1. 测试排序的吞吐量
  2. 测试字符串连接的平均耗时

2.1 排序的吞吐量

  1. 引入pom

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
     <?xml version="1.0" encoding="UTF-8"?>

    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>chapter</artifactId>
    <version>1.0-SNAPSHOT</version>
    <name>chapter</name>
    <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.7</maven.compiler.source>
    <maven.compiler.target>1.7</maven.compiler.target>
    </properties>

    <dependencies>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.4.5</version>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <version>2.4.5</version>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-autoconfigure</artifactId>
    <version>2.4.5</version>
    </dependency>
    <dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-generator-annprocess</artifactId>
    <version>1.33</version>
    <scope>provided</scope>
    </dependency>
    <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
    </dependency>
    <dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-core</artifactId>
    <version>1.33</version>
    <scope>provided</scope>
    </dependency>
    </dependencies>
    </project>
  2. 测试排序的吞吐量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    package org.example;

    import org.junit.jupiter.api.Test;
    import org.openjdk.jmh.annotations.*;
    import org.openjdk.jmh.runner.Runner;
    import org.openjdk.jmh.runner.options.Options;
    import org.openjdk.jmh.runner.options.OptionsBuilder;

    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;
    import java.util.UUID;
    /**
    * @author xiaoyuge
    */
    @State(Scope.Thread)
    public class MyBenchmarkTest {

    private List<String> list;

    /**
    * 使用State方法之前执行的操作
    */
    @Setup
    public void setup() {
    list = new ArrayList<>();
    for (int i = 0; i < 100000; i++) {
    list.add(UUID.randomUUID().toString());
    }
    }
    /**
    * 使用State方法之后执行的操作
    */
    @TearDown
    public void tearDown() {
    list = null;
    }
    /**
    * 测试用例
    */
    @Benchmark
    @BenchmarkMode({Mode.Throughput, Mode.SampleTime, Mode.AverageTime})
    public void testSort() {
    Collections.sort(list);
    }

    @Test
    public void testMyBenchmark() throws Exception {
    Options options = new OptionsBuilder()
    .include(MyBenchmarkTest.class.getSimpleName()) //设置测试类
    .forks(1) //设置启动的JVM进程数量
    .threads(1) //设置启动的线程数量
    .warmupIterations(5) //设置预热次数
    .measurementIterations(5) //正式度量计算的轮数
    .mode(Mode.Throughput) //指标:吞吐量
    .output("/User/xiaoyuge/Desktop/Benchmark.log") //可以指定日志输出地方
    .build();
    new Runner(options).run();
    }
    }
  3. 运行测试方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    # Warmup: 5 iterations, 10 s each           ##5轮, 每轮10s
    # Measurement: 5 iterations, 10 s each
    # Timeout: 10 min per iteration
    # Threads: 1 thread, will synchronize iterations ##线程数量
    # Benchmark mode: Throughput, ops/time ##基准模式: 吞吐量
    # Benchmark: org.example.MyBenchmarkTest.testSort ## 基准测试方法

    # Run progress: 0.00% complete, ETA 00:01:40
    # Fork: 1 of 1 ##启动一个JVM进程做测试

    ##洗面进行了5轮预热,每轮10s
    # Warmup Iteration 1: 401.649 ops/s
    # Warmup Iteration 2: 370.983 ops/s
    # Warmup Iteration 3: 295.298 ops/s
    # Warmup Iteration 4: 316.127 ops/s
    # Warmup Iteration 5: 352.159 ops/s

    ## 测试5轮,每轮10s ,总共50s测试时间
    Iteration 1: 307.553 ops/s
    Iteration 2: 318.164 ops/s
    Iteration 3: 362.988 ops/s
    Iteration 4: 339.201 ops/s
    Iteration 5: 346.274 ops/s

    ## 测试结果:吞吐量为:334.836, 误差范围:85.341
    Result "org.example.MyBenchmarkTest.testSort":
    334.836 ±(99.9%) 85.341 ops/s [Average]
    (min, avg, max) = (307.553, 334.836, 362.988), stdev = 22.163
    CI (99.9%): [249.495, 420.177] (assumes normal distribution)


    # Run complete. Total time: 00:01:41

    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
    MyBenchmarkTest.testSort thrpt 5 334.836 ± 85.341 ops/s

2.2 字符串连接的平均耗时

如下所示设置了Throughput和SampleTime两个BenchmarkMode。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package org.example;

import org.junit.jupiter.api.Test;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

/**
* @author xiaoyuge
*/
@State(Scope.Thread)
public class StringConcatBenchmarkTest {

private String str1;

private String str2;

@Setup
public void setup() {
str1 = "Hello";
str2 = "World";
}

@TearDown
public void tearDown() {
str1 = null;
str2 = null;
}

@Benchmark
@BenchmarkMode({Mode.Throughput, Mode.SampleTime})
public String testStringConcat() {
return str1 + " " + str2;
}

@Test
public void testStringConcatBenchmark() throws Exception {
Options options = new OptionsBuilder()
.include(StringConcatBenchmarkTest.class.getSimpleName())
.forks(1)
.threads(1)
.warmupIterations(5) //预热轮数
.measurementIterations(5) //正式度量计算的轮数
.mode(Mode.AverageTime)
.build();
new Runner(options).run();
}
}

启动测试,输出日志如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# Blackhole mode: full + dont-inline hint (default, use -Djmh.blackhole.autoDetect=true to auto-detect)
# Warmup: 5 iterations, 10 s each
# Measurement: 5 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: org.example.StringConcatBenchmarkTest.testStringConcat

# Run progress: 0.00% complete, ETA 00:01:40
# Fork: 1 of 1
# Warmup Iteration 1: ≈ 10⁻⁸ s/op
# Warmup Iteration 2: ≈ 10⁻⁸ s/op
# Warmup Iteration 3: ≈ 10⁻⁸ s/op
# Warmup Iteration 4: ≈ 10⁻⁸ s/op
# Warmup Iteration 5: ≈ 10⁻⁸ s/op
Iteration 1: ≈ 10⁻⁸ s/op
Iteration 2: ≈ 10⁻⁸ s/op
Iteration 3: ≈ 10⁻⁸ s/op
Iteration 4: ≈ 10⁻⁸ s/op
Iteration 5: ≈ 10⁻⁸ s/op


Result "org.example.StringConcatBenchmarkTest.testStringConcat":
≈ 10⁻⁸ s/op


# Run complete. Total time: 00:01:41

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
StringConcatBenchmarkTest.testStringConcat avgt 5 ≈ 10⁻⁸ s/op