別再寫 main 方法測試了,太 Low!這纔是專業 Java 測試方法!

前言

"If you cannot measure it, you cannot improve it".

在日常開發中,我們對一些代碼的調用或者工具的使用會存在多種選擇方式,在不確定他們性能的時候,我們首先想要做的就是去測量它。大多數時候,我們會簡單的採用多次計數的方式來測量,來看這個方法的總耗時。

但是,如果熟悉JVM類加載機制的話,應該知道JVM默認的執行模式是JIT編譯與解釋混合執行。JVM通過熱點代碼統計分析,識別高頻方法的調用、循環體、公共模塊等,基於JIT動態編譯技術,會將熱點代碼轉換成機器碼,直接交給CPU執行。 在這裏插入圖片描述 也就是說,JVM會不斷的進行編譯優化,這就使得很難確定重複多少次才能得到一個穩定的測試結果?所以,很多有經驗的同學會在測試代碼前寫一段預熱的邏輯。

JMH,全稱 Java Microbenchmark Harness (微基準測試框架),是專門用於Java代碼微基準測試的一套測試工具API,是由 OpenJDK/Oracle 官方發佈的工具。何謂 Micro Benchmark 呢?簡單地說就是在 method 層面上的 benchmark,精度可以精確到微秒級。

Java的基準測試需要注意的幾個點:

  • 測試前需要預熱。
  • 防止無用代碼進入測試方法中。
  • 併發測試。
  • 測試結果呈現。

JMH的使用場景:

  • 定量分析某個熱點函數的優化效果
  • 想定量地知道某個函數需要執行多長時間,以及執行時間和輸入變量的相關性
  • 對比一個函數的多種實現方式

本篇主要是介紹JMH的DEMO演示,和常用的註解參數。希望能對你起到幫助。

DEMO 演示

這裏先演示一個DEMO,讓不瞭解JMH的同學能夠快速掌握這個工具的大概用法。

1.測試項目構建

JMH是內置Java9及之後的版本。這裏是以Java8進行說明。

爲了方便,這裏直接介紹使用maven構建JMH測試項目的方式。

第一種是使用命令行構建,在指定目錄下執行以下命令:

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

對應目錄下會出現一個test項目,打開項目後我們會看到這樣的項目結構。 在這裏插入圖片描述 第二種方式就是直接在現有的maven項目中添加jmh-core和jmh-generator-annprocess的依賴來集成JMH。

   <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>

2.編寫性能測試

這裏我以測試LinkedList 通過index 方式迭代和foreach 方式迭代的性能差距爲例子,編寫測試類,涉及到的註解在之後會講解,

/**
 * @author Richard_yyf
 * @version 1.0 2019/8/27
 */

@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("");
        }
    }
}

3.執行測試

運行 JMH 基準測試有兩種方式,一個是生產jar文件運行,另一個是直接寫main函數或者放在單元測試中執行。

生成jar文件的形式主要是針對一些比較大的測試,可能對機器性能或者真實環境模擬有一些需求,需要將測試方法寫好了放在linux環境執行。具體命令如下

$ mvn clean install
$ java -jar target/benchmarks.jar

我們日常中遇到的一般是一些小測試,比如我上面寫的例子,直接在IDE中跑就好了。啓動方式如下:

 public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(LinkedListIterationBenchMark.class.getSimpleName())
                .forks(1)
                .warmupIterations(2)
                .measurementIterations(2)
             .output("E:/Benchmark.log")
                .build();

        new Runner(opt).run();
    }

4. 報告結果

輸出結果如下,

最後的結果:

Benchmark                                      Mode  Cnt     Score   Error  Units
LinkedListIterationBenchMark.forEachIterate   thrpt    2  1192.380          ops/s
LinkedListIterationBenchMark.forIndexIterate  thrpt    2   206.866          ops/s

整個過程:

# Detecting actual CPU count: 12 detected
# JMH version: 1.21
# VM version: JDK 1.8.0_131, Java HotSpot(TM) 64-Bit Server VM, 25.131-b11
# VM invoker: C:\Program Files\Java\jdk1.8.0_131\jre\bin\java.exe
# VM options: -javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2018.2.2\lib\idea_rt.jar=65175:D:\Program Files\JetBrains\IntelliJ IDEA 2018.2.2\bin -Dfile.encoding=UTF-8
# Warmup: 2 iterations, 10 s each
# Measurement: 2 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 12 threads, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: org.sample.jmh.LinkedListIterationBenchMark.forEachIterate

# Run progress: 0.00% complete, ETA 00:01:20
# Fork: 1 of 1
# Warmup Iteration   1: 1189.267 ops/s
# Warmup Iteration   2: 1197.321 ops/s
Iteration   1: 1193.062 ops/s
Iteration   2: 1191.698 ops/s


Result "org.sample.jmh.LinkedListIterationBenchMark.forEachIterate":
  1192.380 ops/s


# JMH version: 1.21
# VM version: JDK 1.8.0_131, Java HotSpot(TM) 64-Bit Server VM, 25.131-b11
# VM invoker: C:\Program Files\Java\jdk1.8.0_131\jre\bin\java.exe
# VM options: -javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2018.2.2\lib\idea_rt.jar=65175:D:\Program Files\JetBrains\IntelliJ IDEA 2018.2.2\bin -Dfile.encoding=UTF-8
# Warmup: 2 iterations, 10 s each
# Measurement: 2 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 12 threads, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: org.sample.jmh.LinkedListIterationBenchMark.forIndexIterate

# Run progress: 50.00% complete, ETA 00:00:40
# Fork: 1 of 1
# Warmup Iteration   1: 205.676 ops/s
# Warmup Iteration   2: 206.512 ops/s
Iteration   1: 206.542 ops/s
Iteration   2: 207.189 ops/s


Result "org.sample.jmh.LinkedListIterationBenchMark.forIndexIterate":
  206.866 ops/s


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

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
LinkedListIterationBenchMark.forEachIterate   thrpt    2  1192.380          ops/s
LinkedListIterationBenchMark.forIndexIterate  thrpt    2   206.866          ops/s

註解介紹

下面我們來詳細介紹一下相關的註解,

@BenchmarkMode

微基準測試類型。JMH 提供了以下幾種類型進行支持: | 類型 | 描述 | |--|--| |Throughput |每段時間執行的次數,一般是秒 | |AverageTime |平均時間,每次操作的平均耗時 | |SampleTime |在測試中,隨機進行採樣執行的時間 | |SingleShotTime |在每次執行中計算耗時 | |All|所有模式 |

可以註釋在方法級別,也可以註釋在類級別,

@BenchmarkMode(Mode.All)
public class LinkedListIterationBenchMark {
 ...
}
@Benchmark
@BenchmarkMode({Mode.Throughput, Mode.SingleShotTime})
public void m() {
 ...
}

@Warmup

這個單詞的意思就是預熱,iterations = 3就是指預熱輪數。

@Benchmark
@BenchmarkMode({Mode.Throughput, Mode.SingleShotTime})
@Warmup(iterations = 3)
public void m() {
 ...
}

@Measurement

正式度量計算的輪數。

  • iterations 進行測試的輪次
  • time 每輪進行的時長
  • timeUnit時長單位
@Benchmark
@BenchmarkMode({Mode.Throughput, Mode.SingleShotTime})
@Measurement(iterations = 3)
public void m() {
 ...
}

@Threads

每個進程中的測試線程。

@Threads(Threads.MAX)
public class LinkedListIterationBenchMark {
 ...
}

@Fork

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

@Benchmark
@BenchmarkMode({Mode.Throughput, Mode.SingleShotTime})
@Fork(value = 3)
public void m() {
 ...
}

@OutputTimeUnit 基準測試結果的時間類型。一般選擇秒、毫秒、微秒。

@OutputTimeUnit(TimeUnit.SECONDS)
public class LinkedListIterationBenchMark {
 ...
}

@Benchmark

方法級註解,表示該方法是需要進行 benchmark 的對象,用法和 JUnit 的 @Test 類似。

@Param

屬性級註解,@Param 可以用來指定某項參數的多種情況。特別適合用來測試一個函數在不同的參數輸入的情況下的性能。

@Setup

方法級註解,這個註解的作用就是我們需要在測試之前進行一些準備工作,比如對一些數據的初始化之類的。

@TearDown

方法級註解,這個註解的作用就是我們需要在測試之後進行一些結束工作,比如關閉線程池,數據庫連接等的,主要用於資源的回收等。

@State

當使用@Setup參數的時候,必須在類上加這個參數,不然會提示無法運行。

就比如我上面的例子中,就必須設置state。

State 用於聲明某個類是一個“狀態”,然後接受一個 Scope 參數用來表示該狀態的共享範圍。因爲很多 benchmark 會需要一些表示狀態的類,JMH 允許你把這些類以依賴注入的方式注入到 benchmark 函數裏。Scope 主要分爲三種。

  • Thread: 該狀態爲每個線程獨享。
  • Group: 該狀態爲同一個組裏面所有線程共享。
  • Benchmark: 該狀態在所有線程間共享。

啓動方法

在啓動方法中,可以直接指定上述說到的一些參數,並且能將測試結果輸出到指定文件中,

  /**
     * 僅限於IDE中運行
     * 命令行模式 則是 build 然後 java -jar 啓動
     *
     * 1. 這是benchmark 啓動的入口
     * 2. 這裏同時還完成了JMH測試的一些配置工作
     * 3. 默認場景下,JMH會去找尋標註了@Benchmark的方法,可以通過include和exclude兩個方法來完成包含以及排除的語義
     */
    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                // 包含語義
                // 可以用方法名,也可以用XXX.class.getSimpleName()
                .include("Helloworld")
                // 排除語義
                .exclude("Pref")
                // 預熱10輪
                .warmupIterations(10)
                // 代表正式計量測試做10輪,
                // 而每次都是先執行完預熱再執行正式計量,
                // 內容都是調用標註了@Benchmark的代碼。
                .measurementIterations(10)
                //  forks(3)指的是做3輪測試,
                // 因爲一次測試無法有效的代表結果,
                // 所以通過3輪測試較爲全面的測試,
                // 而每一輪都是先預熱,再正式計量。
                .forks(3)
             .output("E:/Benchmark.log")
                .build();

        new Runner(opt).run();
    }

結語

基於JMH可以對很多工具和框架進行測試,比如日誌框架性能對比、BeanCopy性能對比 等,更多的example可以參考官方給出的JMH samples (https://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/)

作者從 Java Developer 角度來談談一些常見的代碼測試陷阱,分析他們和操作系統底層以及 Java 底層的關聯性,並藉助 JMH 來幫助大家擺脫這些陷阱。

作者:Richard_Yi 來源:juejin.cn/post/6844903936869007368

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章