【基準測試】JMH 簡單入門

  • 2019 年 10 月 3 日
  • 筆記

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 程式碼鏈接

關注公眾號「KIWI的碎碎念」,分享的不僅僅是技術