Java微基准测试框架JMH
本文轉(zhuǎn)自:https://www.xncoding.com/2018/01/07/java/jmh.html?
作者:XiongNeng
JMH,即Java Microbenchmark Harness,這是專門用于進(jìn)行代碼的微基準(zhǔn)測(cè)試的一套工具API。
JMH 由 OpenJDK/Oracle 里面那群開發(fā)了 Java 編譯器的大牛們所開發(fā) 。何謂 Micro Benchmark 呢? 簡(jiǎn)單地說就是在 method 層面上的 benchmark,精度可以精確到微秒級(jí)。
Java的基準(zhǔn)測(cè)試需要注意的幾個(gè)點(diǎn):
-
測(cè)試前需要預(yù)熱。
-
防止無用代碼進(jìn)入測(cè)試方法中。
-
并發(fā)測(cè)試。
-
測(cè)試結(jié)果呈現(xiàn)。
比較典型的使用場(chǎng)景:
當(dāng)你已經(jīng)找出了熱點(diǎn)函數(shù),而需要對(duì)熱點(diǎn)函數(shù)進(jìn)行進(jìn)一步的優(yōu)化時(shí),就可以使用 JMH 對(duì)優(yōu)化的效果進(jìn)行定量的分析。
想定量地知道某個(gè)函數(shù)需要執(zhí)行多長(zhǎng)時(shí)間,以及執(zhí)行時(shí)間和輸入 n 的相關(guān)性
一個(gè)函數(shù)有兩種不同實(shí)現(xiàn)(例如JSON序列化/反序列化有Jackson和Gson實(shí)現(xiàn)),不知道哪種實(shí)現(xiàn)性能更好
盡管 JMH 是一個(gè)相當(dāng)不錯(cuò)的 Micro Benchmark Framework,但很無奈的是網(wǎng)上能夠找到的文檔比較少,而官方也沒有提供比較詳細(xì)的文檔,對(duì)使用造成了一定的障礙。 但是有個(gè)好消息是官方的?Code Sample?寫得非常淺顯易懂, 推薦在需要詳細(xì)了解 JMH 的用法時(shí)可以通讀一遍——本文則會(huì)介紹 JMH 最典型的用法和部分常用選項(xiàng)。
第一個(gè)例子
添加maven依賴
如果使用maven項(xiàng)目,只需要添加如下依賴:
<!--?JMH--><dependency><groupId>org.openjdk.jmh</groupId><artifactId>jmh-core</artifactId><version>${jmh.version}</version></dependency><dependency><groupId>org.openjdk.jmh</groupId><artifactId>jmh-generator-annprocess</artifactId><version>${jmh.version}</version><scope>provided</scope></dependency>編寫性能測(cè)試
接下來我寫一個(gè)比較字符串連接操作的時(shí)候,直接使用字符串相加和使用StringBuilder的append方式的性能比較測(cè)試:
/***?比較字符串直接相加和StringBuilder的效率*/@BenchmarkMode(Mode.Throughput)@Warmup(iterations?=?3)@Measurement(iterations?=?10,?time?=?5,?timeUnit?=?TimeUnit.SECONDS)@Threads(8)@Fork(2)@OutputTimeUnit(TimeUnit.MILLISECONDS)public?class?StringBuilderBenchmark?{@Benchmarkpublic?void?testStringAdd()?{String?a?=?"";for?(int?i?=?0;?i?<?10;?i++)?{a?+=?i;}print(a);}@Benchmarkpublic?void?testStringBuilderAdd()?{StringBuilder?sb?=?new?StringBuilder();for?(int?i?=?0;?i?<?10;?i++)?{sb.append(i);}print(sb.toString());}private?void?print(String?a)?{}}執(zhí)行方式
這個(gè)代碼里面有好多注解,你第一次見可能不知道什么意思。先不用管,我待會(huì)一一介紹。
我們來運(yùn)行這個(gè)測(cè)試,運(yùn)行JMH基準(zhǔn)測(cè)試有多種方式,一個(gè)是生成jar文件執(zhí)行, 一個(gè)是直接寫main函數(shù)或?qū)憜卧獪y(cè)試執(zhí)行。
一般對(duì)于大型的測(cè)試,需要測(cè)試時(shí)間比較久,線程比較多的話,就需要去寫好了丟到linux程序里執(zhí)行, 不然本機(jī)執(zhí)行很久時(shí)間什么都干不了了。
mvn?clean?package java?-jar?target/benchmarks.jar先編譯打包之后,然后執(zhí)行就可以了。當(dāng)然在執(zhí)行的時(shí)候可以輸入-h參數(shù)來看幫助。
另外如果對(duì)于一些小的測(cè)試,比如我寫的上面這個(gè)小例子,在IDE里面就可以完成了,丟到linux上去太麻煩。 這時(shí)候可以在里面添加一個(gè)main函數(shù)如下:
public?static?void?main(String[]?args)?throws?RunnerException?{Options?options?=?new?OptionsBuilder().include(StringBuilderBenchmark.class.getSimpleName()).output("E:/Benchmark.log").build();new?Runner(options).run(); }這里其實(shí)也比較簡(jiǎn)單,new個(gè)Options,然后傳入要運(yùn)行哪個(gè)測(cè)試,選擇基準(zhǔn)測(cè)試報(bào)告輸出文件地址,然后通過Runner的run方法就可以跑起來了。
報(bào)告結(jié)果
我們跑一下這個(gè)基準(zhǔn)測(cè)試,完成后打開E:/Benchmark.log,結(jié)果如下:
#?JMH?version:?1.20#?VM?version:?JDK?1.8.0_131,?VM?25.131-b11#?VM?invoker:?C:\Program?Files\Java\jdk1.8.0_131\jre\bin\java.exe#?VM?options:?-javaagent:E:\Program?Files\JetBrains\IntelliJ?IDEA?2017.3\lib\idea_rt.jar=62744:E:\Program?Files\JetBrains\IntelliJ?IDEA?2017.3\bin?-Dfile.encoding=UTF-8#?Warmup:?3?iterations,?1?s?each#?Measurement:?10?iterations,?5?s?each#?Timeout:?10?min?per?iteration#?Threads:?16?threads,?will?synchronize?iterations#?Benchmark?mode:?Throughput,?ops/time#?Benchmark:?com.xncoding.benchmark.string.StringBuilderBenchmark.testStringAdd#?Run?progress:?0.00%?complete,?ETA?00:03:32#?Fork:?1?of?2#?Warmup?Iteration???1:?7332.410?ops/ms#?Warmup?Iteration???2:?8758.506?ops/ms#?Warmup?Iteration???3:?9078.783?ops/msIteration???1:?8824.713?ops/msIteration???2:?9084.977?ops/msIteration???3:?9412.712?ops/msIteration???4:?8843.631?ops/msIteration???5:?9030.556?ops/msIteration???6:?9090.677?ops/msIteration???7:?9493.148?ops/msIteration???8:?8664.593?ops/msIteration???9:?8835.227?ops/msIteration??10:?8570.212?ops/ms#?Run?progress:?25.00%?complete,?ETA?00:03:15#?Fork:?2?of?2#?Warmup?Iteration???1:?5350.686?ops/ms#?Warmup?Iteration???2:?8862.238?ops/ms#?Warmup?Iteration???3:?8086.594?ops/msIteration???1:?9105.306?ops/msIteration???2:?8288.588?ops/msIteration???3:?9307.902?ops/msIteration???4:?9195.150?ops/msIteration???5:?8715.555?ops/msIteration???6:?9075.069?ops/msIteration???7:?9041.037?ops/msIteration???8:?9187.099?ops/msIteration???9:?9145.134?ops/msIteration??10:?9124.229?ops/msResult?"com.xncoding.benchmark.string.StringBuilderBenchmark.testStringAdd":9001.776?±(99.9%)?253.496?ops/ms?[Average](min,?avg,?max)?=?(8288.588,?9001.776,?9493.148),?stdev?=?291.926CI?(99.9%):?[8748.280,?9255.272]?(assumes?normal?distribution)#?JMH?version:?1.20#?VM?version:?JDK?1.8.0_131,?VM?25.131-b11#?VM?invoker:?C:\Program?Files\Java\jdk1.8.0_131\jre\bin\java.exe#?VM?options:?-javaagent:E:\Program?Files\JetBrains\IntelliJ?IDEA?2017.3\lib\idea_rt.jar=62744:E:\Program?Files\JetBrains\IntelliJ?IDEA?2017.3\bin?-Dfile.encoding=UTF-8#?Warmup:?3?iterations,?1?s?each#?Measurement:?10?iterations,?5?s?each#?Timeout:?10?min?per?iteration#?Threads:?16?threads,?will?synchronize?iterations#?Benchmark?mode:?Throughput,?ops/time#?Benchmark:?com.xncoding.benchmark.string.StringBuilderBenchmark.testStringBuilderAdd#?Run?progress:?50.00%?complete,?ETA?00:02:07#?Fork:?1?of?2#?Warmup?Iteration???1:?27202.528?ops/ms#?Warmup?Iteration???2:?26500.586?ops/ms#?Warmup?Iteration???3:?27190.346?ops/msIteration???1:?27891.257?ops/msIteration???2:?28704.541?ops/msIteration???3:?27785.951?ops/msIteration???4:?26841.454?ops/msIteration???5:?26024.288?ops/msIteration???6:?25592.494?ops/msIteration???7:?25626.875?ops/msIteration???8:?25302.248?ops/msIteration???9:?25519.780?ops/msIteration??10:?25275.334?ops/ms#?Run?progress:?75.00%?complete,?ETA?00:01:0279#?Fork:?2?of?2#?Warmup?Iteration???1:?30376.008?ops/ms#?Warmup?Iteration???2:?25131.064?ops/ms#?Warmup?Iteration???3:?25622.342?ops/msIteration???1:?25386.845?ops/msIteration???2:?25825.139?ops/msIteration???3:?26029.607?ops/msIteration???4:?25531.748?ops/msIteration???5:?25374.934?ops/msIteration???6:?25204.530?ops/msIteration???7:?22934.211?ops/msIteration???8:?23907.677?ops/msIteration???9:?24337.963?ops/msIteration??10:?24660.626?ops/msResult?"com.xncoding.benchmark.string.StringBuilderBenchmark.testStringBuilderAdd":25687.875?±(99.9%)?1167.955?ops/ms?[Average](min,?avg,?max)?=?(22934.211,?25687.875,?28704.541),?stdev?=?1345.019CI?(99.9%):?[24519.920,?26855.830]?(assumes?normal?distribution)#?Run?complete.?Total?time:?00:04:08Benchmark?????????????????????????????????????Mode??Cnt??????Score??????Error???UnitsStringBuilderBenchmark.testStringAdd?????????thrpt???20???9001.776?±??253.496??ops/msStringBuilderBenchmark.testStringBuilderAdd??thrpt???20??25687.875?±?1167.955??ops/ms仔細(xì)看,三大部分,第一部分是字符串用加號(hào)連接執(zhí)行的結(jié)果,第二部分是StringBuilder執(zhí)行的結(jié)果,第三部分就是兩個(gè)的簡(jiǎn)單結(jié)果比較。這里注意我們forks傳的2,所以每個(gè)測(cè)試有兩個(gè)fork結(jié)果。
前兩部分是一樣的,簡(jiǎn)單說下。首先會(huì)寫出每部分的一些參數(shù)設(shè)置,然后是預(yù)熱迭代執(zhí)行(Warmup Iteration), 然后是正常的迭代執(zhí)行(Iteration),最后是結(jié)果(Result)。這些看看就好,我們最關(guān)注的就是第三部分, 其實(shí)也就是最終的結(jié)論。千萬別看歪了,他輸出的也確實(shí)很不爽,error那列其實(shí)沒有內(nèi)容,score的結(jié)果是xxx ± xxx,單位是每毫秒多少個(gè)操作??梢钥吹?#xff0c;StringBuilder的速度還確實(shí)是要比String進(jìn)行文字疊加的效率好太多。
注解介紹
好了,當(dāng)你對(duì)JMH有了一個(gè)基本認(rèn)識(shí)后,現(xiàn)在來詳細(xì)解釋一下前面代碼中的各個(gè)注解含義。
@BenchmarkMode
基準(zhǔn)測(cè)試類型。這里選擇的是Throughput也就是吞吐量。根據(jù)源碼點(diǎn)進(jìn)去,每種類型后面都有對(duì)應(yīng)的解釋,比較好理解,吞吐量會(huì)得到單位時(shí)間內(nèi)可以進(jìn)行的操作數(shù)。
-
Throughput: 整體吞吐量,例如“1秒內(nèi)可以執(zhí)行多少次調(diào)用”。
-
AverageTime: 調(diào)用的平均時(shí)間,例如“每次調(diào)用平均耗時(shí)xxx毫秒”。
-
SampleTime: 隨機(jī)取樣,最后輸出取樣結(jié)果的分布,例如“99%的調(diào)用在xxx毫秒以內(nèi),99.99%的調(diào)用在xxx毫秒以內(nèi)”
-
SingleShotTime: 以上模式都是默認(rèn)一次 iteration 是 1s,唯有 SingleShotTime 是只運(yùn)行一次。往往同時(shí)把 warmup 次數(shù)設(shè)為0,用于測(cè)試?yán)鋯?dòng)時(shí)的性能。
-
All(“all”, “All benchmark modes”);
@Warmup
上面我們提到了,進(jìn)行基準(zhǔn)測(cè)試前需要進(jìn)行預(yù)熱。一般我們前幾次進(jìn)行程序測(cè)試的時(shí)候都會(huì)比較慢, 所以要讓程序進(jìn)行幾輪預(yù)熱,保證測(cè)試的準(zhǔn)確性。其中的參數(shù)iterations也就非常好理解了,就是預(yù)熱輪數(shù)。
為什么需要預(yù)熱?因?yàn)?JVM 的 JIT 機(jī)制的存在,如果某個(gè)函數(shù)被調(diào)用多次之后,JVM 會(huì)嘗試將其編譯成為機(jī)器碼從而提高執(zhí)行速度。所以為了讓 benchmark 的結(jié)果更加接近真實(shí)情況就需要進(jìn)行預(yù)熱。
@Measurement
度量,其實(shí)就是一些基本的測(cè)試參數(shù)。
iterations 進(jìn)行測(cè)試的輪次
time 每輪進(jìn)行的時(shí)長(zhǎng)
timeUnit 時(shí)長(zhǎng)單位
都是一些基本的參數(shù),可以根據(jù)具體情況調(diào)整。一般比較重的東西可以進(jìn)行大量的測(cè)試,放到服務(wù)器上運(yùn)行。
@Threads
每個(gè)進(jìn)程中的測(cè)試線程,這個(gè)非常好理解,根據(jù)具體情況選擇,一般為cpu乘以2。
@Fork
進(jìn)行 fork 的次數(shù)。如果 fork 數(shù)是2的話,則 JMH 會(huì) fork 出兩個(gè)進(jìn)程來進(jìn)行測(cè)試。
@OutputTimeUnit
這個(gè)比較簡(jiǎn)單了,基準(zhǔn)測(cè)試結(jié)果的時(shí)間類型。一般選擇秒、毫秒、微秒。
@Benchmark
方法級(jí)注解,表示該方法是需要進(jìn)行 benchmark 的對(duì)象,用法和 JUnit 的 @Test 類似。
@Param
屬性級(jí)注解,@Param 可以用來指定某項(xiàng)參數(shù)的多種情況。特別適合用來測(cè)試一個(gè)函數(shù)在不同的參數(shù)輸入的情況下的性能。
@Setup
方法級(jí)注解,這個(gè)注解的作用就是我們需要在測(cè)試之前進(jìn)行一些準(zhǔn)備工作,比如對(duì)一些數(shù)據(jù)的初始化之類的。
@TearDown
方法級(jí)注解,這個(gè)注解的作用就是我們需要在測(cè)試之后進(jìn)行一些結(jié)束工作,比如關(guān)閉線程池,數(shù)據(jù)庫連接等的,主要用于資源的回收等。
@State
當(dāng)使用@Setup參數(shù)的時(shí)候,必須在類上加這個(gè)參數(shù),不然會(huì)提示無法運(yùn)行。
State 用于聲明某個(gè)類是一個(gè)“狀態(tài)”,然后接受一個(gè) Scope 參數(shù)用來表示該狀態(tài)的共享范圍。 因?yàn)楹芏?benchmark 會(huì)需要一些表示狀態(tài)的類,JMH 允許你把這些類以依賴注入的方式注入到 benchmark 函數(shù)里。Scope 主要分為三種。
Thread: 該狀態(tài)為每個(gè)線程獨(dú)享。
Group: 該狀態(tài)為同一個(gè)組里面所有線程共享。
Benchmark: 該狀態(tài)在所有線程間共享。
關(guān)于State的用法,官方的 code sample 里有比較好的例子。
第二個(gè)例子
再來看一個(gè)更常規(guī)一點(diǎn)性能測(cè)試的例子,
計(jì)算 1 ~ n 之和,比較串行算法和并行算法的效率,看 n 在大約多少時(shí)并行算法開始超越串行算法
首先定義一個(gè)表示這兩種實(shí)現(xiàn)的接口:
/***?Calculator**?@author?XiongNeng*?@version?1.0*?@since?2018/1/7*/ public?interface?Calculator?{/***?calculate?sum?of?an?integer?array**?@param?numbers*?@return*/public?long?sum(int[]?numbers);/***?shutdown?pool?or?reclaim?any?related?resources*/public?void?shutdown(); }具體的兩種實(shí)現(xiàn)代碼我就不貼了,主要說明一下串行算法和并行算法實(shí)現(xiàn)原理:
-
串行算法:使用 for-loop 來計(jì)算 n 個(gè)正整數(shù)之和。
-
并行算法:將所需要計(jì)算的 n 個(gè)正整數(shù)分成 m 份,交給 m 個(gè)線程分別計(jì)算出和以后,再把它們的結(jié)果相加。
進(jìn)行 benchmark 的代碼如下:
/***?自然數(shù)求和的串行和并行算法性能測(cè)試**?@author?XiongNeng*?@version?1.0*?@since?2018/1/7*/ @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MICROSECONDS) @State(Scope.Benchmark) public?class?SecondBenchmark?{@Param({"10000",?"100000",?"1000000"})private?int?length;private?int[]?numbers;private?Calculator?singleThreadCalc;private?Calculator?multiThreadCalc;public?static?void?main(String[]?args)?throws?Exception?{Options?opt?=?new?OptionsBuilder().include(SecondBenchmark.class.getSimpleName()).forks(1).warmupIterations(5).measurementIterations(2)????????????????.build();Collection<RunResult>?results?=??new?Runner(opt).run();ResultExporter.exportResult("單線程與多線程求和性能",?results,?"length",?"微秒");}@Benchmarkpublic?long?singleThreadBench()?{return?singleThreadCalc.sum(numbers);}@Benchmarkpublic?long?multiThreadBench()?{return?multiThreadCalc.sum(numbers);}@Setuppublic?void?prepare()?{numbers?=?IntStream.rangeClosed(1,?length).toArray();singleThreadCalc?=?new?SinglethreadCalculator();multiThreadCalc?=?new?MultithreadCalculator(Runtime.getRuntime().availableProcessors());}@TearDownpublic?void?shutdown()?{singleThreadCalc.shutdown();multiThreadCalc.shutdown();} }我在自己的筆記本電腦上跑下來的結(jié)果,總數(shù)在10000時(shí)并行算法不如串行算法, 總數(shù)達(dá)到100000時(shí)并行算法開始和串行算法接近,總數(shù)達(dá)到1000000時(shí)并行算法所耗時(shí)間約是串行算法的一半左右。
?
參考文章
-
Java使用JMH進(jìn)行簡(jiǎn)單的基準(zhǔn)測(cè)試Benchmark
-
Java 并發(fā)編程筆記:JMH 性能測(cè)試框架
-
JMH - Java Microbenchmark Harness
總結(jié)
以上是生活随笔為你收集整理的Java微基准测试框架JMH的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 简述Linux虚拟内存管理
- 下一篇: Redis 缓存和 MySQL 数据如何