當前位置:
首頁 > 最新 > 使用JMH做基準測試

使用JMH做基準測試

蹲廁所的熊 轉載請註明原創出處,謝謝!


1、簡介

在使用Java編程過程中,我們對於一些代碼調用的細節有多種編寫方式,但是不確定它們性能時,往往採用重複多次計數的方式來解決。但是隨著JVM不斷的進化,隨著代碼執行次數的增加,JVM會不斷的進行編譯優化,使得重複多少次才能夠得到一個穩定的測試結果變得讓人疑惑,這時候有經驗的同學就會在測試執行前先循環上萬次並注釋為預熱。

沒錯!這樣做確實可以獲得一個偏向正確的測試結果,但是我們試想如果每到需要斟酌性能的時候,都要根據場景寫一段預熱的邏輯嗎?當預熱完成後,需要多少次迭代來進行正式內容的測量呢?每次測試結果的輸出報告是不是都需要用 來輸出呢?

其實這些工作都可以交給JMH(the Java Microbenchmark Harness) ,它被作為Java9的一部分來發布,但是我們完全不需要等待Java9,而可以方便的使用它來簡化我們測試,它能夠照看好JVM的預熱、代碼優化,讓你的測試過程變得更加簡單。


2、第一個例子

首先,導入JMH需要的jar包:

接著,我們來試著寫一個例子。就拿最近佔小狼發的Calendar和Joda比較的例子來說吧:

運行結果里確實可以看到Joda的DateTime比Calendar性能要好:

這個例子里的註解都可以換成方法的方式在main方法中指定,比如可以改成這樣:

具體的方法可以自行參考 類,因所有的方法幾乎都可以用註解來實現,所以我們下面著重講解註解的使用。


3、生成jar包執行

對於一些我們自己的一些小測試,直接用上面的方式寫一個main函數手動執行就好了。但是對於大型的測試,需要測試的時間比較久、線程數比較多,加上測試的伺服器需要,一般要放在Linux伺服器里去執行。JMH官方提供了生成jar包的方式來執行。

首先我們需要在maven里增加一個plugin:

接著執行maven的命令生成可執行jar包並執行。

jar的執行命令後面可以加上 來提示可選的命令行參數,用來替換main方法中的方法。


4、IDEA插件

如果你在用Intellij IDEA的話,那麼你可以去plugin里搜JMH來安裝,github地址:https://github.com/artyushov/idea-jmh-plugin

它的主要功能有兩個:

一、幫助你創建@Benchmark方法,可以右鍵點擊 來觸發,也可以使用快捷鍵 。

二、可以讓你像Junit一樣方便的來進行基準測試,不需要寫main方法。點擊某個@Benchmark方法名右鍵run就只會進行游標所在方法的基準測試,而如果游標在類名上,右鍵run的就是整個類的所有基準測試。

5、註解分析

下面我把一些常用的註解全部分析一遍,看完之後你就可以得心應手的使用了。


基準測試類型,對應Mode選項,可用於類或者方法上。 需要注意的是,這個註解的value是一個數組,可以把幾種Mode集合在一起執行,如:

Throughput:整體吞吐量,每秒執行了多少次調用。

AverageTime:用的平均時間,每次操作的平均時間。

SampleTime:隨機取樣,最後輸出取樣結果的分布,例如「99%的調用在xxx毫秒以內,99.99%的調用在xxx毫秒以內」。

SingleShotTime:上模式都是默認一次 iteration 是 1s,唯有 SingleShotTime 是只運行一次。往往同時把 warmup 次數設為0,用於測試冷啟動時的性能。

All:上面的所有模式都執行一次,適用於內部JMH測試。


預熱所需要配置的一些基本測試參數。可用於類或者方法上。一般我們前幾次進行程序測試的時候都會比較慢,所以要讓程序進行幾輪預熱,保證測試的準確性。為什麼需要預熱?因為 JVM 的 JIT 機制的存在,如果某個函數被調用多次之後,JVM 會嘗試將其編譯成為機器碼從而提高執行速度。所以為了讓 benchmark 的結果更加接近真實情況就需要進行預熱。

iterations:預熱的次數。

time:每次預熱的時間。

timeUnit:時間的單位,默認秒。

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


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


每個進程中的測試線程,可用於類或者方法上。一般選擇為cpu乘以2。如果配置了 ,代表使用 個線程。


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


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


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

最後的結果可能是這個樣子的:


@Setup主要實現測試前的初始化工作,只能作用在方法上。用法和Junit一樣。使用該註解必須定義 註解。

@TearDown主要實現測試完成後的垃圾回收等工作,只能作用在方法上。用法和Junit一樣。使用該註解必須定義 註解。

這兩個註解都有一個 的枚舉value,它有三個值(默認的是Trial):

Trial:在每次Benchmark的之前/之後執行。

Iteration:在每次Benchmark的iteration的之前/之後執行。

Invocation:每次調用Benchmark標記的方法之前/之後都會執行。

可見,Level的粒度從Trial到Invocation越來越細。

該註解定義了給定類實例的可用範圍。JMH可以在多線程同時運行的環境測試,因此需要選擇正確的狀態。只能作用在上。被該註解定義的類通常作為 標記的方法的入參,JMH根據scope來進行實例化和共享操作,當然@State可以被繼承使用,如果父類定義了該註解,子類則無需定義。

Scope有如下3種值:

Benchmark:同一個benchmark在多個線程之間共享實例。

Group:同一個線程在同一個group里共享實例。group定義參考註解 。

Thread:不同線程之間的實例不共享。

首先說一下Benchmark,對於同一個@Benchmark,所有線程共享實例,也就是只會new Person 1次

再說一下thread,這個比較好理解,不同線程之間的實例不共享。對於上面我們設定的線程數為8個,也就是會new Person 8次。

而對於Group來說,同一個group的作為一個執行單元,所以 和 共享8個線程,所以一個方法也就會執行new Person 4次。


結合@Benchmark一起使用,把多個基準方法歸為一類,只能作用在方法上。同一個組中的所有測試設置相同的名稱(否則這些測試將獨立運行——沒有任何警告提示!)


定義了多少個線程參與在組中運行基準方法。只能作用在方法上。


這個比較簡單了,基準測試結果的時間類型。可用於類或者方法上。一般選擇秒、毫秒、微秒。


該註解可以控制方法編譯的行為,可用於類或者方法或者構造函數上。它內部有6種模式,這裡我們只關心三種重要的模式:

CompilerControl.Mode.INLINE:強制使用內聯。

CompilerControl.Mode.DONT_INLINE:禁止使用內聯。

CompilerControl.Mode.EXCLUDE:禁止編譯方法。

最後得出的結果也表名,使用內聯優化會影響實際的結果:


6、避免JIT優化

我們在測試的時候,一定要避免JIT優化。對於有一些代碼,編譯器可以推導出一些計算是多餘的,並且完全消除它們。 如果我們的基準測試里有部分代碼被清除了,那測試的結果就不準確了。比如下面這一段代碼:

由於 方法被編譯器優化了,導致效果和 方法一樣變成了空方法,結果也證實了這一點:

如果我們想方法返回值還是void,但是需要讓Math.log(x)的耗時加入到基準運算中,我們可以使用JMH提供給我們的類 ,使用它的 來避免JIT的優化消除。

但是有返回值的方法就不會被優化了嗎?你想的太多了。。。重新改改剛才的代碼,讓欄位 變成final的。

運行後的結果發現 被JIT進行了優化,從 降到了

當然 這種返回寫法和欄位定義成final一樣,都會被進行優化。

優化的原因是因為JVM認為每次計算的結果都是相同的,於是就會把相同代碼移到了JMH的循環之外。

結論:

基準測試方法一定不要返回void。

如果要使用void返回,可以使用 的 來避免JIT的優化消除。

計算不要引用常量,否則會被優化到JMH的循環之外。


7、實戰

最後我們來使用JMH測試不同框架的序列化性能,代碼地址:https://github.com/benjaminwhx/p_rpc/blob/master/serialize/src/main/java/com/github/BenchmarkTest.java

測試結果圖:

測試的主要參數如下:

3個進程,8個線程,每次預熱3次,每次5秒。執行5次,每次5秒,最後算出的值為平均時間,單位納秒。


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 全球大搜羅 的精彩文章:

這是多少人夢想中的家
水清自有活源

TAG:全球大搜羅 |