在日常開發中,我們對一些代碼的調用或者工具的使用會存在多種選擇方式,在不確定他們性能的時候,我們首先想要做的就是去測量它。大多數時候,我們會簡單的采用多次計數的方式來測量,來看這個方法的總耗時。
但是,如果熟悉JVM類加載機制的話,應該知道JVM默認的執行模式是JIT編譯與解釋混合執行。JVM通過熱點代碼統計分析,識別高頻方法的調用、循環體、公共模塊等,基於JIT動態編譯技術,會將熱點代碼轉換成機器碼,直接交給CPU執行。
也就是說,JVM會不斷的進行編譯優化,這就使得很難確定重復多少次才能得到一個穩定的測試結果?所以,很多有經驗的同學會在測試代碼前寫一段預熱的邏輯。
JMH,全稱 Java Microbenchmark Harness (微基准測試框架),是專門用於Java代碼微基准測試的一套測試工具API,是由 OpenJDK/Oracle 官方發布的工具。何謂 Micro Benchmark 呢?簡單地說就是在 method 層面上的 benchmark,精度可以精確到微秒級。
Java的基准測試需要注意的幾個點:
- 測試前需要預熱。
- 防止無用代碼進入測試方法中。
- 並發測試。
- 測試結果呈現。
JMH的使用場景:
- 定量分析某個熱點函數的優化效果
- 想定量地知道某個函數需要執行多長時間,以及執行時間和輸入變量的相關性
- 對比一個函數的多種實現方式
demo
依賴:
<dependency> <groupId>org.openjdk.jmh</groupId> <artifactId>jmh-core</artifactId> <version>${jmh.version}</version> </dependency> <dependency> <groupId>org.openjdk.jmh</groupId> <artifactId>jmh-generator-annprocess</artifactId> <version>${jmh.version}</version> <scope>provided</scope> </dependency>
這里我以測試LinkedList 通過index 方式迭代和foreach 方式迭代的性能差距為例子,編寫測試類
@State(Scope.Benchmark) @OutputTimeUnit(TimeUnit.SECONDS) @Threads(Threads.MAX) public class LinkedListIterationBenchMark { private static final int SIZE = 10000; private List<String> list = new LinkedList<>(); @Setup public void setUp() { for (int i = 0; i < SIZE; i++) { list.add(String.valueOf(i)); } } @Benchmark @BenchmarkMode(Mode.Throughput) public void forIndexIterate() { for (int i = 0; i < list.size(); i++) { list.get(i); System.out.print(""); } } @Benchmark @BenchmarkMode(Mode.Throughput) public void forEachIterate() { for (String s : list) { System.out.print(""); } } }
IDE中測試:
/** * 僅限於IDE中運行 * 命令行模式 則是 build 然后 java -jar 啟動 * <p> * 1. 這是benchmark 啟動的入口 * 2. 這里同時還完成了JMH測試的一些配置工作 * 3. 默認場景下,JMH會去找尋標注了@Benchmark的方法,可以通過include和exclude兩個方法來完成包含以及排除的語義 */ public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder() .include(LinkedListIterationBenchMark.class.getSimpleName()) // 可以用方法名,也可以用XXX.class.getSimpleName() .forks(3)// forks(3)指的是做3輪測試,因為一次測試無法有效的代表結果,所以通過3輪測試較為全面的測試,而每一輪都是先預熱,再正式計量。 .warmupIterations(2)// 預熱2輪 .measurementIterations(2)// 代表正式計量測試做2輪,而每次都是先執行完預熱再執行正式計量, 內容都是調用標注了@Benchmark的代碼。 .output("D:/Benchmark.log") .build(); new Runner(opt).run(); }
測試結果:
Benchmark Mode Cnt Score Error Units
LinkedListIterationBenchMark.forEachIterate thrpt 2 481.343 ops/s
LinkedListIterationBenchMark.forIndexIterate thrpt 2 65.249 ops/s
注解介紹
@BenchmarkMode
微基准測試類型。JMH 提供了以下幾種類型進行支持:
類型 | 描述 |
---|---|
Throughput | 每段時間執行的次數,一般是秒 |
AverageTime | 平均時間,每次操作的平均耗時 |
SampleTime | 在測試中,隨機進行采樣執行的時間 |
SingleShotTime | 在每次執行中計算耗時 |
All | 所有模式 |
@Warmup
這個單詞的意思就是預熱,iterations = 3
就是指預熱輪數。
@Measurement
正式度量計算的輪數。
iterations
進行測試的輪次time
每輪進行的時長timeUnit
時長單位
@Threads
每個進程中的測試線程。
@Fork
進行 fork 的次數。如果 fork 數是3的話,則 JMH 會 fork 出3個進程來進行測試。
@OutputTimeUnit
基准測試結果的時間類型。一般選擇秒、毫秒、微秒。
@Benchmark
方法級注解,表示該方法是需要進行 benchmark 的對象,用法和 JUnit 的 @Test
類似。
@Param
屬性級注解,@Param
可以用來指定某項參數的多種情況。特別適合用來測試一個函數在不同的參數輸入的情況下的性能。
@Setup
方法級注解,這個注解的作用就是我們需要在測試之前進行一些准備工作,比如對一些數據的初始化之類的。
@TearDown
方法級注解,這個注解的作用就是我們需要在測試之后進行一些結束工作,比如關閉線程池,數據庫連接等的,主要用於資源的回收等。
@State
當使用@Setup
參數的時候,必須在類上加這個參數,不然會提示無法運行。
就比如我上面的例子中,就必須設置state
。
State
用於聲明某個類是一個“狀態”,然后接受一個 Scope 參數用來表示該狀態的共享范圍。因為很多 benchmark 會需要一些表示狀態的類,JMH 允許你把這些類以依賴注入的方式注入到 benchmark 函數里。Scope 主要分為三種。
- Thread: 該狀態為每個線程獨享。
- Group: 該狀態為同一個組里面所有線程共享。
- Benchmark: 該狀態在所有線程間共享。
參考:juejin.cn/post/6844903936869007368
更多的example可以參考官方給出的JMH samples (https://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/)