JMH Benchmark 效能測試淺談

FeelTouch發表於2018-10-25

概述

 

轉自:https://testerhome.com/topics/11250


JMH 是一個由 OpenJDK/Oracle 裡面那群開發了 Java 編譯器的大牛們所開發的 Micro Benchmark Framework 。何謂 Micro Benchmark 呢?簡單地說就是在 method 層面上的 benchmark,精度可以精確到微秒級。可以看出 JMH 主要使用在當你已經找出了熱點函式,而需要對熱點函式進行進一步的優化時,就可以使用 JMH 對優化的效果進行定量的分析。

典型場景

  1. 想定量地知道某個函式需要執行多長時間,以及執行時間和輸入 n 的相關性
  2. 一個函式有兩種不同實現(例如實現 A 使用了 FixedThreadPool,實現 B 使用了 ForkJoinPool),不知道哪種實現效能更好

儘管 JMH 是一個相當不錯的 Micro Benchmark Framework,但很無奈的是網上能夠找到的文件比較少,而官方也沒有提供比較詳細的文件,對使用造成了一定的障礙。但是有個好訊息是官方的 Code Sample 寫得非常淺顯易懂,推薦在需要詳細瞭解 JMH 的用法時可以通讀一遍——本文則會介紹 JMH 最典型的用法和部分常用選項。

使用
我在idea中使用的時候 ,需要在pom.xml中使用如下配置:

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

實際測試程式碼如下:(建立第一個BenchMark)

package normaltest;

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;

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Thread)
public class JMHFirstBenchmark {
    @Benchmark//對要被測試效能的程式碼新增註解,說明該方法是要被測試效能的
    public int sleepAWhile() {
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            // ignore
        }
        return 0;
    }

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(JMHFirstBenchmark.class.getSimpleName())
                .forks(1)
                .warmupIterations(3)
                .measurementIterations(3)
                .build();

        new Runner(opt).run();
    }

}

輸出結果:

# JMH version: 1.19
# VM version: JDK 1.8.0_144, VM 25.144-b01
# VM invoker: /usr/lib/jvm/java-8-oracle/jre/bin/java
# VM options: -javaagent:/home/lijun/Downloads/idea-IU-172.3317.76/lib/idea_rt.jar=36941:/home/lijun/Downloads/idea-IU-172.3317.76/bin -Dfile.encoding=UTF-8
# Warmup: 3 iterations, 1 s each
# Measurement: 3 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: normaltest.JMHFirstBenchmark.sleepAWhile

# Run progress: 0.00% complete, ETA 00:00:06
# Fork: 1 of 1
# Warmup Iteration   1: 50189.958 us/op
# Warmup Iteration   2: 50074.371 us/op
# Warmup Iteration   3: 50075.263 us/op
Iteration   1: 50071.800 us/op
Iteration   2: 50074.029 us/op
Iteration   3: 50072.798 us/op

Result "normaltest.JMHFirstBenchmark.sleepAWhile":
  50072.876 ±(99.9%) 20.367 us/op [Average]
  (min, avg, max) = (50071.800, 50072.876, 50074.029), stdev = 1.116
  CI (99.9%): [50052.509, 50093.242] (assumes normal distribution)

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

Benchmark                      Mode  Cnt      Score    Error  Units
JMHFirstBenchmark.sleepAWhile  avgt    3  50072.876 ± 20.367  us/op

對 sleepAWhile() 的測試結果顯示執行時間平均約為50毫秒。因為我們的測試物件 sleepAWhile() 正好就是睡眠50毫秒,所以 JMH 顯示的結果可以說很符合我們的預期。

基本概念:
Mode 
Mode 表示 JMH 進行 Benchmark 時所使用的模式。通常是測量的維度不同,或是測量的方式不同。目前 JMH 共有四種模式:
(1).Throughput: 整體吞吐量,例如“1秒內可以執行多少次呼叫”。
(2).AverageTime: 呼叫的平均時間,例如“每次呼叫平均耗時xxx毫秒”。
(3).SampleTime: 隨機取樣,最後輸出取樣結果的分佈,例如“99%的呼叫在xxx毫秒以內,99.99%的呼叫在xxx毫秒以內”
(4).SingleShotTime: 以上模式都是預設一次 iteration 是 1s,唯有 SingleShotTime 是隻執行一次。往往同時把 warmup 次數設為0,用於測試冷啟動時的效能。
Interation 
Iteration是JMH進行測試的最小單位。大部分模式下,iteration代表的是一秒,JMH會在這一秒內不斷呼叫需要benchmark的方法,然後根據模式對其取樣,計算吞吐量,計算平均執行時間等。
Warmup 
Warmup是指在實際進行Benchmark前先進行預熱的行為。因為JVM的JIT機制的存在,如果某個函式被呼叫多次以後,JVM會嘗試將其編譯成為機器碼從而提高執行速度。所以為了讓benchmark的結果更加接近真實情況就需要進行預熱。

註解
現在來解釋一下上面例子中使用到的註解,其實很多註解的意義完全可以望文生義 :)
@Benchmark 
表示該方法是需要進行 benchmark 的物件,用法和 JUnit 的 @Test 類似。
@Mode
Mode 如之前所說,表示 JMH 進行 Benchmark 時所使用的模式。
@State
State 用於宣告某個類是一個“狀態”,然後接受一個 Scope 引數用來表示該狀態的共享範圍。因為很多 benchmark 會需要一些表示狀態的類,JMH 允許你把這些類以依賴注入的方式注入到 benchmark 函式裡。Scope 主要分為兩種。

(1).Thread: 該狀態為每個執行緒獨享。
(2).Benchmark: 該狀態在所有執行緒間共享。
關於State的用法,官方的 code sample 裡有比較好的例子。

@OutputTimeUnit
benchmark 結果所使用的時間單位。

啟動選項

解釋完了註解,再來看看 JMH 在啟動前設定的引數。

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

new Runner(opt).run();
<font color=red>include</font>

benchmark 所在的類的名字,注意這裡是使用正規表示式對所有類進行匹配的。

fork
進行 fork 的次數。如果 fork 數是2的話,則 JMH 會 fork 出兩個程式來進行測試。

warmupIterations
預熱的迭代次數。

measurementIterations
實際測量的迭代次數。

相關文章