【基準測試】JMH 簡單入門

JMH 簡單入門

什麼是 JMH

JMH 是 Java Microbenchmark Harness 的縮寫。中文意思大致是 “JAVA 微基準測試套件”。首先先明白什麼是“基準測試”。百度百科給的定義如下:

基準測試是指通過設計科學的測試方法、測試工具和測試系統,實現對一類測試對象的某項性能指標進行定量的和可對比的測試。

可以簡單的類比成我們電腦常用的魯大師,或者手機常用的跑分軟件安兔兔之類的性能檢測軟件。都是按一定的基準或者在特定條件下去測試某一對象的的性能,比如顯卡、IO、CPU之類的。

爲什麼要使用 JMH

基準測試的特質有如下幾種:

①、可重複性:可進行重複性的測試,這樣做有利於比較每次的測試結果,得到性能結果的長期變化趨勢,爲系統調優和上線前的容量規劃做參考。

②、可觀測性:通過全方位的監控(包括測試開始到結束,執行機、服務器、數據庫),及時瞭解和分析測試過程發生了什麼。

③、可展示性:相關人員可以直觀明瞭的瞭解測試結果(web界面、儀表盤、折線圖樹狀圖等形式)。

④、真實性:測試的結果反映了客戶體驗到的真實的情況(真實準確的業務場景+與生產一致的配置+合理正確的測試方法)。

⑤、可執行性:相關人員可以快速的進行測試驗證修改調優(可定位可分析)。

可見要做一次符合特質的基準測試,是很繁瑣也很困難的。外界因素很容易影響到最終的測試結果。特別對於 JAVA的基準測試。


有些文章會告訴我們 JAVA是 C++編寫的,一般來說 JAVA編寫的程序不太可能比 C++編寫的代碼運行效率更好。但是JAVA在某些場景的確要比 C++運行的更高效。不要覺得天方夜譚。其實 JVM隨着這些年的發展已經變得很智能,它會在運行期間不斷的去優化。


這對於我們程序來說是好事,但是對於性能測試就頭疼的。你運行的次數與時間不同可能獲得的結果也不同,很難獲得一個比較穩定的結果。對於這種情況,有一個解決辦法就是大量的重複調用,並且在真正測試前還要進行一定的預熱,使結果儘可能的準確。

除了這些,對於結果我們還需要一個很好的展示,可以讓我們通過這些展示結果判斷性能的好壞。

**而這些JMH都有!**😊

如何使用 JMH

下面我們以字符串拼接的幾種方法爲例子使用JMH做基準測試。

1. 導入依賴

JMH是 JDK9自帶的,如果你是 JDK9 之前的版本也可以通過導入 openjdk

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

2. 目錄結構

.
├── pom.xml
└── src
   ├── main
   │  └── java
   │     └── cn
   │        └── coder4j
   │           └── study
   │              └── demo
   │                 └── jmh
   │                    ├── benchmark
   │                    │  └── StringConnectBenchmark.java
   │                    └── runner
   │                       └── StringBuilderRunner.java
   └── test
      └── java
         └── cn
            └── coder4j
               └── study
                  └── demo

3. 具體代碼

  • StringBuilderRunner.java
/**
 * coder4j.cn
 * Copyright (C) 2013-2018 All Rights Reserved.
 */
package cn.coder4j.study.demo.jmh.runner;

import cn.coder4j.study.demo.jmh.benchmark.StringConnectBenchmark;
import org.openjdk.jmh.annotations.Mode;
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;

/**
 * @author buhao
 * @version StringBuilderRunner.java, v 0.1 2018-12-25 09:53 buhao
 */
public class StringBuilderRunner {

    public static void main( String[] args ) throws RunnerException {
        Options opt = new OptionsBuilder()
                // 導入要測試的類
                .include(StringConnectBenchmark.class.getSimpleName())
                // 預熱5輪
                .warmupIterations(5)
                // 度量10輪
                .measurementIterations(10)
                .mode(Mode.Throughput)
                .forks(3)
                .build();

        new Runner(opt).run();


    }

}
  • StringConnectBenchmark.java
/**
 * coder4j.cn
 * Copyright (C) 2013-2018 All Rights Reserved.
 */
package cn.coder4j.study.demo.jmh.benchmark;

import org.openjdk.jmh.annotations.Benchmark;

/**
 * @author buhao
 * @version StringConnectBenchmark.java, v 0.1 2018-12-25 09:29 buhao
 */
public class StringConnectBenchmark {

    /**
     * 字符串拼接之 StringBuilder 基準測試
     */
    @Benchmark
    public void testStringBuilder() {
        print(new StringBuilder().append(1).append(2).append(3).toString());
    }

    /**
     * 字符串拼接之直接相加基準測試
     */
    @Benchmark
    public void testStringAdd() {
        print(new String()+ 1 + 2 + 3);
    }

    /**
     * 字符串拼接之String Concat基準測試
     */
    @Benchmark
    public void testStringConcat() {
        print(new String().concat("1").concat("2").concat("3"));
    }

    /**
     * 字符串拼接之 StringBuffer 基準測試
     */
    @Benchmark
    public void testStringBuffer() {
        print(new StringBuffer().append(1).append(2).append(3).toString());
    }

    /**
     * 字符串拼接之 StringFormat 基準測試
     */
    @Benchmark
    public void testStringFormat(){
        print(String.format("%s%s%s", 1, 2, 3));
    }

    public void print(String str) {

    }
}

4. 運行結果

# Run progress: 93.33% complete, ETA 00:00:15
# Fork: 3 of 3
objc[12440]: Class JavaLaunchHelper is implemented in both /Library/Java/JavaVirtualMachines/jdk1.8.0_91.jdk/Contents/Home/jre/bin/java (0x106a7d4c0) and /Library/Java/JavaVirtualMachines/jdk1.8.0_91.jdk/Contents/Home/jre/lib/libinstrument.dylib (0x106af74e0). One of the two will be used. Which one is undefined.
# Warmup Iteration   1: 747281.755 ops/s
# Warmup Iteration   2: 924220.081 ops/s
# Warmup Iteration   3: 1129741.585 ops/s
# Warmup Iteration   4: 1135268.541 ops/s
# Warmup Iteration   5: 1062994.936 ops/s
Iteration   1: 1142834.160 ops/s
Iteration   2: 1143207.472 ops/s
Iteration   3: 1178363.827 ops/s
Iteration   4: 1156408.897 ops/s
Iteration   5: 1123123.829 ops/s
Iteration   6: 1086029.992 ops/s
Iteration   7: 1108795.147 ops/s
Iteration   8: 1125522.731 ops/s
Iteration   9: 1120021.744 ops/s
Iteration  10: 1119916.181 ops/s


Result "cn.coder4j.study.demo.jmh.benchmark.StringConnectBenchmark.testStringFormat":
  1132633.183 ±(99.9%) 16252.303 ops/s [Average]
  (min, avg, max) = (1082146.355, 1132633.183, 1182418.648), stdev = 24325.684
  CI (99.9%): [1116380.879, 1148885.486] (assumes normal distribution)


# Run complete. Total time: 00:03:57

Benchmark                                  Mode  Cnt          Score         Error  Units
StringConnectBenchmark.testStringAdd      thrpt   30   63728919.269 ±  906608.141  ops/s
StringConnectBenchmark.testStringBuffer   thrpt   30  112423521.098 ± 1157072.848  ops/s
StringConnectBenchmark.testStringBuilder  thrpt   30  110558976.274 ±  654163.111  ops/s
StringConnectBenchmark.testStringConcat   thrpt   30   44820009.200 ±  524305.660  ops/s
StringConnectBenchmark.testStringFormat   thrpt   30    1132633.183 ±   16252.303  ops/s

5. 代碼解析

  • StringBuilderRunner

這個 runner 類的作用,就是啓動基準測試。


JMH 通常有兩種方式啓動,一種就是通過命令行使用 maven 命令執行。這種適合對於大型基準測試,像那些要運行很多很多次,並且運行的時間也很長的情況下。你可以直接打個 jar包,發到服務器上,敲個命令就不用管它,過幾十分鐘、幾小時、幾天的時間再回來看結果。

但是很多情況下,我們只是想簡單測試一個小功能,沒必要還要搞臺服務器去跑。所以 JMH 還提供了一種通過 Main方法運行的方式,就如上面代碼所示。

在 Main 方法中,通過 org.openjdk.jmh.runner.Runner 類去運行 org.openjdk.jmh.runner.options.Options 實例即可。這裏的重點在於 Options 對象的構建。官方提供了一個OptionsBuilder對象去構建。這個 Builder對象是流式的。它的常用方法及對應的註解形式如下:

方法名 參數 作用 對應註解
include 要運行基準測試類的簡單名稱 eg. StringConnectBenchmark 指定要運行的基準測試類 -
exclude 不要運行基準測試類的簡單名稱 eg. StringConnectBenchmark 指定不要運行的基準測試類 -
warmupIterations 預熱的迭代次數 指定預熱的迭代次數 @Warmup
warmupBatchSize 預熱批量的大小 指定預熱批量的大小 @Warmup
warmupForks 預熱模式:INDI,BULK,BULK_INDI 指定預熱模式 @Warmup
warmupMode 預熱的模式 指定預熱的模式 @Warmup
warmupTime 預熱的時間 指定預熱的時間 @Warmup
measurementIterations 測試的迭代次數 指定測試的迭代次數 @Measurement
measurementBatchSize 測試批量的大小 指定測試批量的大小 @Measurement
measurementTime 測試的時間 指定測試的時間 @Measurement
mode 測試模式: Throughput(吞吐量), AverageTime(平均時間),SampleTime(在測試中,隨機進行採樣執行的時間),SingleShotTime(在每次執行中計算耗時),All 指定測試的模式 @BenchmarkMode
  • StringConnectBenchmark


這個就是真正執行基準測試的類,這個類很像單元測試的類,每個測試方法中寫上你要執行的測試代碼。只不過這裏把@Test換成了@Benchmark註解。


而加上了這個就指明這個方法是基準測試方法,當 Runner類的 Main方法運行時,它就會找這些被註解修飾的方法,再按指定的規則去進行基準測試。當然可能不同的方法有時候需要不同的規則,這個時間可以通過上面方法對應的註解形式去單獨指定某個方法的規則即可。

6. 結果解析


結果主要分成三個部分。


第一部分以 “#Warmup Iteration。。。。”這種形式的內容。這表明每次預熱迭代的結果。


另一部分以“Iteration。。。”形式內容,這表明每次基準測試迭代的結果。

最後一部分以“Result。。。”形式的內容,這就是所有迭代跑完最終的結果。第一段結果告訴了我們最大值、最小值、平均值的信息。


而最最後的表格結構的信息纔是我們分析的重點,但是它輸出的結果有點錯位,剛開始我一直在糾結 Error是±  906608.141代表什麼意思,google了一圈發現,Error它其實什麼都沒輸出,而且 Score 是63728919.269 ±  906608.141。我用表格排板了一下,解釋如下:

Benchmark Mode Cnt Score Error Units
基準測試執行的方法 測試模式,這裏是吞吐量 運行多少次 分數 錯誤 單位
StringConnectBenchmark.testStringAdd thrpt 30 63728919.269 ±  906608.141 ops/s
StringConnectBenchmark.testStringBuffer thrpt 30 112423521.098 ± 1157072.848 ops/s
StringConnectBenchmark.testStringBuilder thrpt 30 110558976.274 ±  654163.111 ops/s
StringConnectBenchmark.testStringConcat thrpt 30 44820009.200 ±  524305.660 ops/s
StringConnectBenchmark.testStringFormat thrpt 30 1132633.183 ±   16252.303 ops/s

結論:

StringBuffer  >= StringBuilder > String直接相加 > StringConcat >> StringFormat

可見 StringBuffer 與 StringBuilder 大致性能相同,都比直接相加高几個數量級,而且直接相加與 Concat 方法相加差不多。但是這裏不管哪種都比 StringFormat高 N 個數量級。所以 String的 Format方法一定要慎用、不用、禁用!!!

相關鏈接

參考鏈接

  1. openjdk 官方 DEMO

  2. openjdk 官方 DEMO (翻譯版)

  3. 淺談基準測試

  4. 什麼是基準測試

  5. JMH 學習筆記   ← 很不錯

  6. 使用 JMH 做 JAVA 基準測試

  7. JMH 做 JAVA 基準測試

代碼鏈接

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