跳到主要內容

性能調優必備利器之 JMH

if 快還是 switch 快?HashMap 的初始化 size 要不要指定,指定之後性能可以提高多少?各種序列化方法哪個耗時更短?


無論出自何種原因需要進行性能評估,量化指標總是必要的。


在大部分場合,簡單地回答誰快誰慢是遠遠不夠的,如何將程序性能量化呢?


這就需要我們的主角 JMH 登場了!


JMH 簡介


JMH(Java Microbenchmark Harness)是用於代碼微基準測試的工具套件,主要是基於方法層面的基準測試,精度可以達到納秒級。該工具是由 Oracle 內部實現 JIT 的大牛們編寫的,他們應該比任何人都了解 JIT 以及 JVM 對於基準測試的影響。


當你定位到熱點方法,希望進一步優化方法性能的時候,就可以使用 JMH 對優化的結果進行量化的分析。


JMH 比較典型的應用場景如下:



  1. 想準確地知道某個方法需要執行多長時間,以及執行時間和輸入之間的相關性

  2. 對比接口不同實現在給定條件下的吞吐量

  3. 查看多少百分比的請求在多長時間內完成


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


加入依賴


因為 JMH 是 JDK9 自帶的,如果是 JDK9 之前的版本需要加入如下依賴(目前 JMH 的最新版本為 1.23):


<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.23</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.23</version>
</dependency>

編寫基準測試


接下來,創建一個 JMH 測試類,用來判斷 +StringBuilder.append() 兩種字符串拼接哪個耗時更短,具體代碼如下所示:


@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 5, time = 5)
@Threads(4)
@Fork(1)
@State(value = Scope.Benchmark)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class StringConnectTest {

@Param(value = {"10", "50", "100"})
private int length;

@Benchmark
public void testStringAdd(Blackhole blackhole) {
String a = "";
for (int i = 0; i < length; i++) {
a += i;
}
blackhole.consume(a);
}

@Benchmark
public void testStringBuilderAdd(Blackhole blackhole) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
sb.append(i);
}
blackhole.consume(sb.toString());
}

public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(StringConnectTest.class.getSimpleName())
.result("result.json")
.resultFormat(ResultFormatType.JSON).build();
new Runner(opt).run();
}
}

其中需要測試的方法用 @Benchmark 註解標識,這些註解的具體含義將在下面介紹。


在 main() 函數中,首先對測試用例進行配置,使用 Builder 模式配置測試,將配置參數存入 Options 對象,並使用 Options 對象構造 Runner 啟動測試。


另外大家可以看下官方提供的 jmh 示例 demo:http://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/


執行基準測試


準備工作做好了,接下來,運行代碼,等待片刻,測試結果就出來了,下面對結果做下簡單說明:


# JMH version: 1.23
# VM version: JDK 1.8.0_201, Java HotSpot(TM) 64-Bit Server VM, 25.201-b09
# VM invoker: D:\Software\Java\jdk1.8.0_201\jre\bin\java.exe
# VM options: -javaagent:D:\Software\JetBrains\IntelliJ IDEA 2019.1.3\lib\idea_rt.jar=61018:D:\Software\JetBrains\IntelliJ IDEA 2019.1.3\bin -Dfile.encoding=UTF-8
# Warmup: 3 iterations, 1 s each
# Measurement: 5 iterations, 5 s each
# Timeout: 10 min per iteration
# Threads: 4 threads, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: com.wupx.jmh.StringConnectTest.testStringBuilderAdd
# Parameters: (length = 100)

該部分為測試的基本信息,比如使用的 Java 路徑,預熱代碼的迭代次數,測量代碼的迭代次數,使用的線程數量,測試的統計單位等。


# Warmup Iteration   1: 1083.569 ±(99.9%) 393.884 ns/op
# Warmup Iteration 2: 864.685 ±(99.9%) 174.120 ns/op
# Warmup Iteration 3: 798.310 ±(99.9%) 121.161 ns/op

該部分為每一次熱身中的性能指標,預熱測試不會作為最終的統計結果。預熱的目的是讓 JVM 對被測代碼進行足夠多的優化,比如,在預熱后,被測代碼應該得到了充分的 JIT 編譯和優化。


Iteration   1: 810.667 ±(99.9%) 51.505 ns/op
Iteration 2: 807.861 ±(99.9%) 13.163 ns/op
Iteration 3: 851.421 ±(99.9%) 33.564 ns/op
Iteration 4: 805.675 ±(99.9%) 33.038 ns/op
Iteration 5: 821.020 ±(99.9%) 66.943 ns/op

Result "com.wupx.jmh.StringConnectTest.testStringBuilderAdd":
819.329 ±(99.9%) 72.698 ns/op [Average]
(min, avg, max) = (805.675, 819.329, 851.421), stdev = 18.879
CI (99.9%): [746.631, 892.027] (assumes normal distribution)

Benchmark (length) Mode Cnt Score Error Units
StringConnectTest.testStringBuilderAdd 100 avgt 5 819.329 ± 72.698 ns/op

該部分显示測量迭代的情況,每一次迭代都显示了當前的執行速率,即一個操作所花費的時間。在進行 5 次迭代后,進行統計,在本例中,length 為 100 的情況下 testStringBuilderAdd 方法的平均執行花費時間為 819.329 ns,誤差為 72.698 ns


最後的測試結果如下所示:


Benchmark                               (length)  Mode  Cnt     Score     Error  Units
StringConnectTest.testStringAdd 10 avgt 5 161.496 ± 17.097 ns/op
StringConnectTest.testStringAdd 50 avgt 5 1854.657 ± 227.902 ns/op
StringConnectTest.testStringAdd 100 avgt 5 6490.062 ± 327.626 ns/op
StringConnectTest.testStringBuilderAdd 10 avgt 5 68.769 ± 4.460 ns/op
StringConnectTest.testStringBuilderAdd 50 avgt 5 413.021 ± 30.950 ns/op
StringConnectTest.testStringBuilderAdd 100 avgt 5 819.329 ± 72.698 ns/op

結果表明,在拼接字符次數越多的情況下,StringBuilder.append() 的性能就更好。


生成 jar 包執行


對於一些小測試,直接用上面的方式寫一個 main 函數手動執行就好了。


對於大型的測試,需要測試的時間比較久、線程數比較多,加上測試的服務器需要,一般要放在 Linux 服務器里去執行。


JMH 官方提供了生成 jar 包的方式來執行,我們需要在 maven 里增加一個 plugin,具體配置如下:


<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.4.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<finalName>jmh-demo</finalName>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>org.openjdk.jmh.Main</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>

接着執行 maven 的命令生成可執行 jar 包並執行:


mvn clean install
java -jar target/jmh-demo.jar StringConnectTest

JMH 基礎


為了能夠更好地使用 JMH 的各項功能,下面對 JMH 的基本概念進行講解:


@BenchmarkMode


用來配置 Mode 選項,可用於類或者方法上,這個註解的 value 是一個數組,可以把幾種 Mode 集合在一起執行,如:@BenchmarkMode({Mode.SampleTime, Mode.AverageTime}),還可以設置為 Mode.All,即全部執行一遍。



  1. Throughput:整體吞吐量,每秒執行了多少次調用,單位為 ops/time

  2. AverageTime:用的平均時間,每次操作的平均時間,單位為 time/op

  3. SampleTime:隨機取樣,最後輸出取樣結果的分佈

  4. SingleShotTime:只運行一次,往往同時把 Warmup 次數設為 0,用於測試冷啟動時的性能

  5. All:上面的所有模式都執行一次


@State


通過 State 可以指定一個對象的作用範圍,JMH 根據 scope 來進行實例化和共享操作。@State 可以被繼承使用,如果父類定義了該註解,子類則無需定義。由於 JMH 允許多線程同時執行測試,不同的選項含義如下:



  1. Scope.Benchmark:所有測試線程共享一個實例,測試有狀態實例在多線程共享下的性能

  2. Scope.Group:同一個線程在同一個 group 里共享實例

  3. Scope.Thread:默認的 State,每個測試線程分配一個實例


@OutputTimeUnit


為統計結果的時間單位,可用於類或者方法註解


@Warmup


預熱所需要配置的一些基本測試參數,可用於類或者方法上。一般前幾次進行程序測試的時候都會比較慢,所以要讓程序進行幾輪預熱,保證測試的準確性。參數如下所示:



  1. iterations:預熱的次數

  2. time:每次預熱的時間

  3. timeUnit:時間的單位,默認秒

  4. batchSize:批處理大小,每次操作調用幾次方法



為什麼需要預熱?


因為 JVM 的 JIT 機制的存在,如果某個函數被調用多次之後,JVM 會嘗試將其編譯為機器碼,從而提高執行速度,所以為了讓 benchmark 的結果更加接近真實情況就需要進行預熱。



@Measurement


實際調用方法所需要配置的一些基本測試參數,可用於類或者方法上,參數和 @Warmup 相同。


@Threads


每個進程中的測試線程,可用於類或者方法上。


@Fork


進行 fork 的次數,可用於類或者方法上。如果 fork 數是 2 的話,則 JMH 會 fork 出兩個進程來進行測試。


@Param


指定某項參數的多種情況,特別適合用來測試一個函數在不同的參數輸入的情況下的性能,只能作用在字段上,使用該註解必須定義 @State 註解。


在介紹完常用的註解后,讓我們來看下 JMH 有哪些陷阱。


JMH 陷阱


在使用 JMH 的過程中,一定要避免一些陷阱。


比如 JIT 優化中的死碼消除,比如以下代碼:


@Benchmark
public void testStringAdd(Blackhole blackhole) {
String a = "";
for (int i = 0; i < length; i++) {
a += i;
}
}

JVM 可能會認為變量 a 從來沒有使用過,從而進行優化把整個方法內部代碼移除掉,這就會影響測試結果。


JMH 提供了兩種方式避免這種問題,一種是將這個變量作為方法返回值 return a,一種是通過 Blackhole 的 consume 來避免 JIT 的優化消除。


其他陷阱還有常量摺疊與常量傳播、永遠不要在測試中寫循環、使用 Fork 隔離多個測試方法、方法內聯、偽共享與緩存行、分支預測、多線程測試等,感興趣的可以閱讀 https://github.com/lexburner/JMH-samples 了解全部的陷阱。


JMH 插件


大家還可以通過 IDEA 安裝 JMH 插件使 JMH 更容易實現基準測試,在 IDEA 中點擊 File->Settings...->Plugins,然後搜索 jmh,選擇安裝 JMH plugin:



這個插件可以讓我們能夠以 JUnit 相同的方式使用 JMH,主要功能如下:



  1. 自動生成帶有 @Benchmark 的方法

  2. 像 JUnit 一樣,運行單獨的 Benchmark 方法

  3. 運行類中所有的 Benchmark 方法


比如可以通過右鍵點擊 Generate...,選擇操作 Generate JMH benchmark 就可以生成一個帶有 @Benchmark 的方法。


還有將光標移動到方法聲明並調用 Run 操作就運行一個單獨的 Benchmark 方法。


將光標移到類名所在行,右鍵點擊 Run 運行,該類下的所有被 @Benchmark 註解的方法都會被執行。


JMH 可視化


除此以外,如果你想將測試結果以圖表的形式可視化,可以試下這些網站:



  • JMH Visual Chart:http://deepoove.com/jmh-visual-chart/

  • JMH Visualizer:https://jmh.morethan.io/


比如將上面測試例子結果的 json 文件導入,就可以實現可視化:



總結


本文主要介紹了性能基準測試工具 JMH,它可以通過一些功能來規避由 JVM 中的 JIT 或者其他優化對性能測試造成的影響。


只需要將待測的業務邏輯用 @Benchmark 註解標識,就可以讓 JMH 的註解處理器自動生成真正的性能測試代碼,以及相應的性能測試配置文件。


最好的關係就是互相成就,大家的在看、轉發、留言三連就是我創作的最大動力。



參考


http://openjdk.java.net/projects/code-tools/jmh/


深入拆解Java虛擬機


《實戰Java高併發程序設計》


本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】



※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面



網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!



※想知道最厲害的網頁設計公司"嚨底家"!



※別再煩惱如何寫文案,掌握八大原則!



※產品缺大量曝光嗎?你需要的是一流包裝設計!




Orignal From: 性能調優必備利器之 JMH

留言

這個網誌中的熱門文章

有了四步解題法模板,再也不害怕動態規劃!(看不懂算我輸)

導言 動態規劃問題一直是算法面試當中的重點和難點,並且動態規劃這種通過空間換取時間的算法思想在實際的工作中也會被頻繁用到,這篇文章的目的主要是解釋清楚 什麼是動態規劃 ,還有就是面對一道動態規劃問題,一般的 思考步驟 以及其中的注意事項等等,最後通過幾道題目將理論和實踐結合。 什麼是動態規劃 如果你還沒有聽說過動態規劃,或者僅僅只有耳聞,或許你可以看看 Quora 上面的這個 回答 。 How to explain dynamic 用一句話解釋動態規劃就是 " 記住你之前做過的事 ",如果更準確些,其實是 " 記住你之前得到的答案 "。 我舉個大家工作中經常遇到的例子。 在軟件開發中,大家經常會遇到一些系統配置的問題,配置不對,系統就會報錯,這個時候一般都會去 Google 或者是查閱相關的文檔,花了一定的時間將配置修改好。 過了一段時間,去到另一個系統,遇到類似的問題,這個時候已經記不清之前修改過的配置文件長什麼樣,這個時候有兩種方案,一種方案還是去 Google 或者查閱文檔,另一種方案是借鑒之前修改過的配置,第一種做法其實是萬金油,因為你遇到的任何問題其實都可以去 Google,去查閱相關文件找答案,但是這會花費一定的時間,相比之下,第二種方案肯定會更加地節約時間,但是這個方案是有條件的,條件如下: 之前的問題和當前的問題有着關聯性,換句話說,之前問題得到的答案可以幫助解決當前問題 需要記錄之前問題的答案 當然在這個例子中,可以看到的是,上面這兩個條件均滿足,大可去到之前配置過的文件中,將配置拷貝過來,然後做些細微的調整即可解決當前問題,節約了大量的時間。 不知道你是否從這些描述中發現,對於一個動態規劃問題,我們只需要從兩個方面考慮,那就是 找出問題之間的聯繫 ,以及 記錄答案 ,這裏的難點其實是找出問題之間的聯繫,記錄答案只是順帶的事情,利用一些簡單的數據結構就可以做到。 概念 上面的解釋如果大家可以理解的話,接    動態規劃 算法是通過拆分問題,定義問題狀態和狀態之間的關係,使得問題能夠以遞推(或者說分治)的方式去解決。它的幾個重要概念如下所述。    階段: 對於一個完整的問題過程,適當的切分為若干個相互聯繫的子問題,每次在求解一個子問題...

計算機本地文件快要滅絕了

   編者按: 文件是数字世界的基石,是我們基本的工作單位。但是,隨着互聯網的雲化、平台化、服務化,文件日益變得可有可無。這樣一種改變究竟好不好呢?喜歡懷舊的 Simon Pitt 開始回顧各種文件的好處,哪怕這讓他顯得不合時宜。原文發表在 medium 上,標題是:Computer Files Are Going Extinct   我喜歡文件。我喜歡對文件重命名、移動、排序,改變它們在文件夾中的显示方式,去備份文件,將之上傳到互聯網,恢復它們,對其進行複製,甚至還可以對文件進行碎片整理。作為信息存儲方式的一種隱喻,在我看來文件是很出色的。我喜歡把文件當作一個工作單位。如果我要寫篇文章,文章會放在文件裏面。如果我要生成圖像,圖像會保存進文件裏面。    謳歌 files.doc   文件是擬物化的。這是個很花哨的詞,只是用來表示文件是反映現實物品的一個数字概念。比方說,Word 文檔就像一張紙,躺在你的辦公桌上(desktop)。JPEG 就像一幅畫,等等。它們每個都有一個小圖標,圖標的樣子看起來像它們所代表的現實物品。一堆紙,一個畫框,一個馬尼拉文件夾。真的挺很迷人的。   我喜歡文件的一點是,不管裏面有什麼,跟文件的交互方式總是一致的。我上面提到的那些東西——複製、排序、碎片整理——我可以對任何文件進行那些處理。文件可能是圖像、遊戲的一部分、也可能是我最喜歡的餐具清單。碎片整理程序不在乎它是什麼。它不會去判斷內容。   自從我開始在 Windows 95 裏面創建文件以來,我就一直都很喜歡文件。但是我注意到我們已經開始慢慢地遠離把文件當作基本工作單位的做法。 Windows95。我的計算機    services.mp3 的興起   十幾歲的時候,我開始痴迷於收集和管理数字音樂:我收藏 MP3 文件。一大堆的 128 kbps MP3 文件。如果你足夠幸運,有自己的 CD 刻錄機的話,就可以將它們刻錄到 CD 上,然後在朋友之間傳遞。一張 CD 可以容納 700 MB。這相當於將近 500 張軟盤!   我會仔細端詳我的收藏,然後煞費苦心地給它們添加上 IDv1 和 IDv2 音樂標籤。隨着時間的流逝,大家開始開發可以在雲端自動獲取曲目列表的工具,這樣你就可以檢查和驗證 MP3 的質量。有時候我甚至會去聽那些該死的東西,儘管...

純電動 Mini Cooper SE 將成為中國國產車,年產 16 萬輛

BMW 集團與中國長城汽車合資,將於江蘇建立新廠,專門投入生產 MINI Cooper SE 和部分長城品牌電動車,預計於 2022 年完工並投入生產,每年將可生產 16 萬輛電動車。 靈動可愛的 Mini Cooper,在許多車迷心中都有著特殊的地位,今年 7 月發表了首款純電動版本的 Mini Cooper SE 之後,獲得熱烈迴響,預訂數量已接近 8 萬台,顯示大家對於純電 Mini 的熱愛,因為油電版的 Mini Cooper Countryman 的全球總銷售量也才 3 萬出頭。 Mini Cooper SE 之前公布了官方定價,最低從 27,900 歐元起算,美國售價約 29,900 美元。相比現有的三門款,只貴了一成左右。然而,三年後,中國消費者將有機會買到最便宜的電動 Mini。 電動 Mini Cooper SE 最低價是 27,900 歐元,扣掉全額補助最低可以到 24,400 歐元。 BMW 集團與中國長城汽車集團於 2018 年宣布,將組建合資公司光束汽車,投入在中國的電動車生產計畫,而現在他們正式宣布啟動計畫,於江蘇張家港打造一個新工廠,全部投入電動車的製造,包括了 Mini Cooper SE 和其他長城汽車旗下的電動車。 目前的電動 Mini 只在英國牛津工廠製造,不難想像當產能轉移到中國後,Mini Cooper SE 的價格將有機會進一步調降,來競爭全球最大的電動車市場。這座屬於合資公司光束汽車的新工廠,採用一個新的產銷模式,由 BMW 和長城共同合作開發、設計、製造新產品,但是銷售通路完全沿用原本的品牌渠道。 換句話說,2020 年到 2022 年銷售的電動 Mini,將會是英國製造,而 2022 年後就會有中國製造版本開賣,考量到 Mini 在中國每年約有 30 萬輛的銷售額,同時油電版的 Coutryman 銷量更佔了全球將近五分之一,無怪乎 BMW 會想在最接近主要市場的地方蓋工廠囉。 外型完美復刻油車版 最後,簡單介紹一下 Mini Cooper SE 這台車。Mini 在電動化的路上,盡力保持著跟經典造型一致的設計,畢竟大家愛的就是它的設計。電動版的 Mini 車頭、車身跟車屁股都多了一個黃色的插頭標誌,車頭的氣壩則變成封閉式設計,除此之外,幾乎看不出來差別,連馬達...