1. 程式人生 > 實用技巧 >使用JMH微基準測試

使用JMH微基準測試

概述

JMH只適合細粒度的方法測試,並不適用於系統之間的鏈路測試

使用Maven搭建基準測試專案骨架

JMH官方推薦使用Maven來搭建基準測試的骨架,使用也很簡單,使用如下命令來生成maven專案:

mvn archetype:generate -DinteractiveMode=false -DarchetypeGroupId=org.openjdk.jmh -DarchetypeArtifactId=jmh-java-benchmark-archetype -DgroupId=org.sample -DartifactId=jmh-benchmark -Dversion=1.0

上面的maven命令使用了jmh-java-benchmark-archetype來生成java語言的基準測試骨架,如果使用其他語言可以將這個引數對應替換,所有可選引數參考

jmh,生成的專案groupId是org.sample,artifaceId是test,執行完之後會在當前目錄下生成一個test目錄,切換到test目錄下執行mvn clean install就會生成benchmarks.jar,再使用java -jar benchmarks.jar就可以執行基準測試了。

JMH引數配置

如果你想直接在已有maven專案中整合JMH,那也很簡單,手動在POM檔案中新增以下兩個依賴就行了,

      <dependency>
        <groupId>org.openjdk.jmh</groupId>
        <artifactId>jmh-core</artifactId>
        <version>1.26</version>
      </dependency>

      <dependency>
        <groupId>org.openjdk.jmh</groupId>
        <artifactId>jmh-generator-annprocess</artifactId>
        <version>1.26</version>
        <scope>provided</scope>
      </dependency>

從maven archetype外掛生成的pom檔案來看,這個工程使用了maven-shade-plugin來將所有的依賴打包到同一個jar包中,並在Manifest檔案中配置了Main-Class屬性,這樣就能直接通過java -jar命令來執行了。

註解說明

@Benchmark

@Benchmark標籤是用來標記測試方法的,只有被這個註解標記的話,該方法才會參與基準測試,但是有一個基本的原則就是被@Benchmark標記的方法必須是public的。

@Warmup

@Warmup用來配置預熱的內容,可用於類或者方法上,越靠近執行方法的地方越準確。一般配置warmup的引數有這些:

  • iterations:預熱的次數。
  • time:每次預熱的時間。
  • timeUnit:時間單位,預設是s。
  • batchSize:批處理大小,每次操作呼叫幾次方法。(後面用到)

@Measurement

用來控制實際執行的內容,配置的選項本warmup一樣。

@BenchmarkMode

@BenchmarkMode主要是表示測量的緯度,有以下這些緯度可供選擇:

  • Mode.Throughput 吞吐量緯度
  • Mode.AverageTime 平均時間
  • Mode.SampleTime 抽樣檢測
  • Mode.SingleShotTime 檢測一次呼叫
  • Mode.All 運用所有的檢測模式 在方法級別指定@BenchmarkMode的時候可以一定指定多個緯度,例如: @BenchmarkMode({Mode.Throughput, Mode.AverageTime, Mode.SampleTime, Mode.SingleShotTime}),代表同時在多個緯度對目標方法進行測量。

@OutputTimeUnit

@OutputTimeUnit代表測量的單位,比如秒級別,毫秒級別,微妙級別等等。一般都使用微妙和毫秒級別的稍微多一點。該註解可以用在方法級別和類級別,當用在類級別的時候會被更加精確的方法級別的註解覆蓋,原則就是離目標更近的註解更容易生效。

@State

在很多時候我們需要維護一些狀態內容,比如在多執行緒的時候我們會維護一個共享的狀態,這個狀態值可能會在每隔執行緒中都一樣,也有可能是每個執行緒都有自己的狀態,JMH為我們提供了狀態的支援。該註解只能用來標註在類上,因為類作為一個屬性的載體。 @State的狀態值主要有以下幾種:

  • Scope.Benchmark 該狀態的意思是會在所有的Benchmark的工作執行緒中共享變數內容。
  • Scope.Group 同一個Group的執行緒可以享有同樣的變數
  • Scope.Thread 每隔執行緒都享有一份變數的副本,執行緒之間對於變數的修改不會相互影響。 下面看兩個常見的@State的寫法:

容器測試

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Warmup;
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;

/**
 * @copyright: Copyright (c) 2020
 * @company: 上海匯石數字科技有限公司
 * @description:
 * @author: Qi.Hong
 * @create: 2020-10-13 12:15
 */

@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
public class CollectionsTest {
	private static final int TEN_MILLION = 10000000;
	@Benchmark
	@BenchmarkMode(Mode.AverageTime)
	@OutputTimeUnit(TimeUnit.MICROSECONDS)
	public void arrayList() {
		List<String> array = new ArrayList<>();
		for (int i = 0; i < TEN_MILLION; i++) {
			array.add("123");
		}
	}
	@Benchmark
	@BenchmarkMode(Mode.AverageTime)
	@OutputTimeUnit(TimeUnit.MICROSECONDS)
	public void arrayListSize() {
		List<String> array = new ArrayList<>(TEN_MILLION);
		for (int i = 0; i < TEN_MILLION; i++) {
			array.add("123");
		}
	}
	public static void main(String[] args) throws RunnerException {
		Options opt = new OptionsBuilder()
				.include(CollectionsTest.class.getSimpleName())
				.forks(1)
				.build();
		new Runner(opt).run();
	}
}

輸出:

# JMH version: 1.26
# VM version: JDK 1.8.0_261, Java HotSpot(TM) 64-Bit Server VM, 25.261-b12
# VM invoker: D:\Scoop\apps\oraclejdk8\current\jre\bin\java.exe
# VM options: -javaagent:D:\Program Files (x86)\JetBrains\IntelliJ IDEA\lib\idea_rt.jar=2510:D:\Program Files (x86)\JetBrains\IntelliJ IDEA\bin -Dfile.encoding=UTF-8
# Warmup: 5 iterations, 1 s each
# Measurement: 5 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: org.sample.CollectionsTest.arrayList

# Run progress: 0.00% complete, ETA 00:00:20
# Fork: 1 of 1
# Warmup Iteration   1: 98276.345 us/op
# Warmup Iteration   2: 78334.454 us/op
# Warmup Iteration   3: 60003.018 us/op
# Warmup Iteration   4: 69836.653 us/op
# Warmup Iteration   5: 56462.906 us/op
Iteration   1: 55989.247 us/op
Iteration   2: 57849.294 us/op
Iteration   3: 54504.816 us/op
Iteration   4: 55541.316 us/op
Iteration   5: 56276.563 us/op


Result "org.sample.CollectionsTest.arrayList":
  56032.247 ±(99.9%) 4691.335 us/op [Average]
  (min, avg, max) = (54504.816, 56032.247, 57849.294), stdev = 1218.325
  CI (99.9%): [51340.913, 60723.582] (assumes normal distribution)


# JMH version: 1.26
# VM version: JDK 1.8.0_261, Java HotSpot(TM) 64-Bit Server VM, 25.261-b12
# VM invoker: D:\Scoop\apps\oraclejdk8\current\jre\bin\java.exe
# VM options: -javaagent:D:\Program Files (x86)\JetBrains\IntelliJ IDEA\lib\idea_rt.jar=2510:D:\Program Files (x86)\JetBrains\IntelliJ IDEA\bin -Dfile.encoding=UTF-8
# Warmup: 5 iterations, 1 s each
# Measurement: 5 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: org.sample.CollectionsTest.arrayListSize

# Run progress: 50.00% complete, ETA 00:00:13
# Fork: 1 of 1
# Warmup Iteration   1: 37526.226 us/op
# Warmup Iteration   2: 36804.161 us/op
# Warmup Iteration   3: 91434.682 us/op
# Warmup Iteration   4: 465368.225 us/op
# Warmup Iteration   5: 206318.400 us/op
Iteration   1: 207185.940 us/op
Iteration   2: 168980.250 us/op
Iteration   3: 103344.225 us/op
Iteration   4: 34195.040 us/op
Iteration   5: 34570.855 us/op


Result "org.sample.CollectionsTest.arrayListSize":
  109655.262 ±(99.9%) 300766.041 us/op [Average]
  (min, avg, max) = (34195.040, 109655.262, 207185.940), stdev = 78107.986
  CI (99.9%): [≈ 0, 410421.303] (assumes normal distribution)


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

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
CollectionsTest.arrayList      avgt    5   56032.247 ±   4691.335  us/op
CollectionsTest.arrayListSize  avgt    5  109655.262 ± 300766.041  us/op

Process finished with exit code 0

參考

使用JMH微基準測試一切

基於Maven的JMH的搭建和使用

常見的集合容器應當避免的坑

JMH官方案例

基準測試神器-JMH