正文開始
JMH,即Java Microbenchmark Harness什乙,這是專門用于進行代碼的微基準測試的一套工具API客冈。
JMH 由 OpenJDK/Oracle 里面那群開發(fā)了 Java 編譯器的大牛們所開發(fā) 。何謂 Micro Benchmark 呢稳强? 簡單地說就是在 method 層面上的 benchmark场仲,精度可以精確到微秒級。
Java的基準測試需要注意的幾個點:
測試前需要預熱退疫。
防止無用代碼進入測試方法中渠缕。
并發(fā)測試。
測試結(jié)果呈現(xiàn)褒繁。
比較典型的使用場景:
當你已經(jīng)找出了熱點函數(shù)亦鳞,而需要對熱點函數(shù)進行進一步的優(yōu)化時,就可以使用 JMH 對優(yōu)化的效果進行定量的分析棒坏。
想定量地知道某個函數(shù)需要執(zhí)行多長時間燕差,以及執(zhí)行時間和輸入 n 的相關(guān)性
一個函數(shù)有兩種不同實現(xiàn)(例如JSON序列化/反序列化有Jackson和Gson實現(xiàn)),不知道哪種實現(xiàn)性能更好
盡管 JMH 是一個相當不錯的 Micro Benchmark Framework坝冕,但很無奈的是網(wǎng)上能夠找到的文檔比較少徒探,而官方也沒有提供比較詳細的文檔,對使用造成了一定的障礙喂窟。 但是有個好消息是官方的 Code Sample 寫得非常淺顯易懂测暗, 推薦在需要詳細了解 JMH 的用法時可以通讀一遍——本文則會介紹 JMH 最典型的用法和部分常用選項央串。
第一個例子
添加maven依賴
如果使用maven項目,只需要添加如下依賴:
編寫性能測試
執(zhí)行方式
這個代碼里面有好多注解碗啄,你第一次見可能不知道什么意思质和。先不用管,我待會一一介紹稚字。
我們來運行這個測試饲宿,運行JMH基準測試有多種方式,一個是生成jar文件執(zhí)行胆描, 一個是直接寫main函數(shù)或?qū)憜卧獪y試執(zhí)行褒傅。
一般對于大型的測試,需要測試時間比較久袄友,線程比較多的話,就需要去寫好了丟到linux程序里執(zhí)行霹菊, 不然本機執(zhí)行很久時間什么都干不了了剧蚣。
先編譯打包之后,然后執(zhí)行就可以了旋廷。當然在執(zhí)行的時候可以輸入-h參數(shù)來看幫助鸠按。
另外如果對于一些小的測試,比如我寫的上面這個小例子饶碘,在IDE里面就可以完成了目尖,丟到linux上去太麻煩。 這時候可以在里面添加一個main函數(shù)如下:
這里其實也比較簡單扎运,new個Options瑟曲,然后傳入要運行哪個測試,選擇基準測試報告輸出文件地址豪治,然后通過Runner的run方法就可以跑起來了洞拨。
報告結(jié)果
我們跑一下這個基準測試,完成后打開 E:/Benchmark.log
负拟,結(jié)果如下:
仔細看烦衣,三大部分,第一部分是字符串用加號連接執(zhí)行的結(jié)果掩浙,第二部分是StringBuilder執(zhí)行的結(jié)果花吟,第三部分就是兩個的簡單結(jié)果比較。這里注意我們forks傳的2厨姚,所以每個測試有兩個fork結(jié)果衅澈。
前兩部分是一樣的,簡單說下谬墙。首先會寫出每部分的一些參數(shù)設(shè)置矾麻,然后是預熱迭代執(zhí)行(Warmup Iteration)纱耻, 然后是正常的迭代執(zhí)行(Iteration),最后是結(jié)果(Result)险耀。這些看看就好弄喘,我們最關(guān)注的就是第三部分, 其實也就是最終的結(jié)論甩牺。千萬別看歪了蘑志,他輸出的也確實很不爽,error那列其實沒有內(nèi)容贬派,score的結(jié)果是xxx ± xxx急但,單位是每毫秒多少個操作「惴Γ可以看到波桩,StringBuilder的速度還確實是要比String進行文字疊加的效率好太多。
注解介紹
好了请敦,當你對JMH有了一個基本認識后镐躲,現(xiàn)在來詳細解釋一下前面代碼中的各個注解含義。
@BenchmarkMode
基準測試類型侍筛。這里選擇的是Throughput也就是吞吐量萤皂。根據(jù)源碼點進去,每種類型后面都有對應的解釋匣椰,比較好理解裆熙,吞吐量會得到單位時間內(nèi)可以進行的操作數(shù)。
Throughput: 整體吞吐量禽笑,例如“1秒內(nèi)可以執(zhí)行多少次調(diào)用”入录。
AverageTime: 調(diào)用的平均時間,例如“每次調(diào)用平均耗時xxx毫秒”佳镜。
SampleTime: 隨機取樣纷跛,最后輸出取樣結(jié)果的分布,例如“99%的調(diào)用在xxx毫秒以內(nèi)邀杏,99.99%的調(diào)用在xxx毫秒以內(nèi)”
SingleShotTime: 以上模式都是默認一次 iteration 是 1s贫奠,唯有 SingleShotTime 是只運行一次。往往同時把 warmup 次數(shù)設(shè)為0望蜡,用于測試冷啟動時的性能唤崭。
All(“all”, “All benchmark modes”);
@Warmup
上面我們提到了,進行基準測試前需要進行預熱脖律。一般我們前幾次進行程序測試的時候都會比較慢谢肾, 所以要讓程序進行幾輪預熱,保證測試的準確性小泉。其中的參數(shù)iterations也就非常好理解了芦疏,就是預熱輪數(shù)冕杠。
為什么需要預熱?因為 JVM 的 JIT 機制的存在酸茴,如果某個函數(shù)被調(diào)用多次之后分预,JVM 會嘗試將其編譯成為機器碼從而提高執(zhí)行速度。所以為了讓 benchmark 的結(jié)果更加接近真實情況就需要進行預熱薪捍。
@Measurement
度量笼痹,其實就是一些基本的測試參數(shù)。
iterations 進行測試的輪次
time 每輪進行的時長
timeUnit 時長單位
都是一些基本的參數(shù)酪穿,可以根據(jù)具體情況調(diào)整凳干。一般比較重的東西可以進行大量的測試,放到服務(wù)器上運行被济。
@Threads
每個進程中的測試線程救赐,這個非常好理解,根據(jù)具體情況選擇只磷,一般為cpu乘以2经磅。
@Fork
進行 fork 的次數(shù)。如果 fork 數(shù)是2的話喳瓣,則 JMH 會 fork 出兩個進程來進行測試。
@OutputTimeUnit
這個比較簡單了赞别,基準測試結(jié)果的時間類型畏陕。一般選擇秒、毫秒仿滔、微秒惠毁。
@Benchmark
方法級注解,表示該方法是需要進行 benchmark 的對象崎页,用法和 JUnit 的 @Test 類似鞠绰。
@Param
屬性級注解,@Param 可以用來指定某項參數(shù)的多種情況飒焦。特別適合用來測試一個函數(shù)在不同的參數(shù)輸入的情況下的性能蜈膨。
@Setup
方法級注解,這個注解的作用就是我們需要在測試之前進行一些準備工作牺荠,比如對一些數(shù)據(jù)的初始化之類的翁巍。
@TearDown
方法級注解,這個注解的作用就是我們需要在測試之后進行一些結(jié)束工作休雌,比如關(guān)閉線程池灶壶,數(shù)據(jù)庫連接等的,主要用于資源的回收等杈曲。
@State
當使用@Setup參數(shù)的時候驰凛,必須在類上加這個參數(shù)胸懈,不然會提示無法運行。
State 用于聲明某個類是一個“狀態(tài)”恰响,然后接受一個 Scope 參數(shù)用來表示該狀態(tài)的共享范圍趣钱。 因為很多 benchmark 會需要一些表示狀態(tài)的類,JMH 允許你把這些類以依賴注入的方式注入到 benchmark 函數(shù)里渔隶。Scope 主要分為三種羔挡。
Thread: 該狀態(tài)為每個線程獨享。
Group: 該狀態(tài)為同一個組里面所有線程共享间唉。
Benchmark: 該狀態(tài)在所有線程間共享绞灼。
關(guān)于State的用法,官方的 code sample 里有比較好的例子呈野。
第二個例子
再來看一個更常規(guī)一點性能測試的例子低矮,
計算 1 ~ n 之和,比較串行算法和并行算法的效率被冒,看 n 在大約多少時并行算法開始超越串行算法
首先定義一個表示這兩種實現(xiàn)的接口:
具體的兩種實現(xiàn)代碼我就不貼了军掂,主要說明一下串行算法和并行算法實現(xiàn)原理:
串行算法:使用 for-loop 來計算 n 個正整數(shù)之和。
并行算法:將所需要計算的 n 個正整數(shù)分成 m 份昨悼,交給 m 個線程分別計算出和以后蝗锥,再把它們的結(jié)果相加。
進行 benchmark 的代碼如下:
看到這里還沒過癮率触,那么就來群里與更多的大佬交流切磋技術(shù)终议,戳這里:咱們來一起抱團取暖,好嗎葱蝗?
我在自己的筆記本電腦上跑下來的結(jié)果穴张,總數(shù)在10000時并行算法不如串行算法, 總數(shù)達到100000時并行算法開始和串行算法接近两曼,總數(shù)達到1000000時并行算法所耗時間約是串行算法的一半左右皂甘。