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方法一定要慎用、不用、禁用!!!
相關鏈接
參考鏈接
-
JMH 學習筆記 ← 很不錯
代碼鏈接
關注公眾號「KIWI的碎碎念」,分享的不僅僅是技術