Java JMH進(jìn)行基準(zhǔn)測(cè)試的使用小結(jié)
在 Java 的依賴庫(kù)中,有個(gè)大名鼎鼎的 JMH(Java Microbenchmark Harness),是由 Java虛擬機(jī)團(tuán)隊(duì)開發(fā)的 Java 基準(zhǔn)測(cè)試工具。
在 JMH 中,正如 單元測(cè)試框架 JUnit 一樣,我們也可以通過大量的注解來進(jìn)行一定的配置,一個(gè)典型的 JMH 程序執(zhí)行如下圖所示[2]:
也即,通過開啟多個(gè)進(jìn)程,多個(gè)線程,先執(zhí)行預(yù)熱,然后執(zhí)行迭代,最后匯總所有的測(cè)試數(shù)據(jù)進(jìn)行分析,這就是 JMH 的執(zhí)行流程,聽起來是不是不難理解。
1.示例
學(xué)習(xí)新技能通常先通過一個(gè) case 來幫準(zhǔn)我們?cè)趺从?,有什么結(jié)果,這里我們通過改寫官方的一個(gè) sample 來看看。
package org.example; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.Measurement; import org.openjdk.jmh.annotations.Warmup; import org.openjdk.jmh.results.format.ResultFormatType; 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; import java.util.concurrent.TimeUnit; @Warmup(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS) @Measurement(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) public class HelloWorldBenchmark { private static int num = 0; @Benchmark public void helloWorld() { ++num; } public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder() .include(HelloWorldBenchmark.class.getSimpleName()) .forks(1) .result("helloWorld.json") .resultFormat(ResultFormatType.JSON) .build(); new Runner(opt).run(); } }
示例很簡(jiǎn)單,就是簡(jiǎn)單對(duì) static 變量做自增,最后將結(jié)果輸出到 json 文件中,下面是運(yùn)行結(jié)果:
# JMH version: 1.23 # VM version: JDK 21, Java HotSpot(TM) 64-Bit Server VM, 21+35-LTS-2513 # VM invoker: C:\Program Files\Java\jdk-21\bin\java.exe # VM options: -javaagent:D:\chromedownload\ideaIC-2023.2.3.win\lib\idea_rt.jar=55507:D:\chromedownload\ideaIC-2023.2.3.win\bin -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8 # Warmup: 1 iterations, 1 s each # Measurement: 2 iterations, 1 s each # Timeout: 10 min per iteration # Threads: 1 thread, will synchronize iterations # Benchmark mode: Throughput, ops/time # Benchmark: org.example.HelloWorldBenchmark.helloWorld # Run progress: 0.00% complete, ETA 00:00:03 # Fork: 1 of 1 # Warmup Iteration 1: 1659899825.872 ops/s Iteration 1: 1646745186.884 ops/s Iteration 2: 1681125023.980 ops/s Result "org.example.HelloWorldBenchmark.helloWorld": 1663935105.432 ops/s # Run complete. Total time: 00:00:03 REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial experiments, perform baseline and negative tests that provide experimental control, make sure the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts. Do not assume the numbers tell you what you want them to tell. Benchmark Mode Cnt Score Error Units HelloWorldBenchmark.helloWorld thrpt 2 1663935105.432 ops/s Benchmark result is saved to helloWorld.json
通過簡(jiǎn)單的設(shè)置,我們?cè)诨鶞?zhǔn)測(cè)試中可以看到多次測(cè)試的每秒吞吐量,最后結(jié)果輸出到 helloWorldjson 文件:
[ { "jmhVersion" : "1.23", "benchmark" : "org.example.HelloWorldBenchmark.helloWorld", "mode" : "thrpt", "threads" : 1, "forks" : 1, "jvm" : "C:\Program Files\Java\jdk-21\bin\java.exe", "jvmArgs" : [ "-javaagent:D:\chromedownload\ideaIC-2023.2.3.win\lib\idea_rt.jar=55507:D:\chromedownload\ideaIC-2023.2.3.win\bin", "-Dfile.encoding=UTF-8", "-Dsun.stdout.encoding=UTF-8", "-Dsun.stderr.encoding=UTF-8" ], "jdkVersion" : "21", "vmName" : "Java HotSpot(TM) 64-Bit Server VM", "vmVersion" : "21+35-LTS-2513", "warmupIterations" : 1, "warmupTime" : "1 s", "warmupBatchSize" : 1, "measurementIterations" : 2, "measurementTime" : "1 s", "measurementBatchSize" : 1, "primaryMetric" : { "score" : 1.6639351054317546E9, "scoreError" : "NaN", "scoreConfidence" : [ "NaN", "NaN" ], "scorePercentiles" : { "0.0" : 1.6467451868835843E9, "50.0" : 1.6639351054317546E9, "90.0" : 1.6811250239799252E9, "95.0" : 1.6811250239799252E9, "99.0" : 1.6811250239799252E9, "99.9" : 1.6811250239799252E9, "99.99" : 1.6811250239799252E9, "99.999" : 1.6811250239799252E9, "99.9999" : 1.6811250239799252E9, "100.0" : 1.6811250239799252E9 }, "scoreUnit" : "ops/s", "rawData" : [ [ 1.6467451868835843E9, 1.6811250239799252E9 ] ] }, "secondaryMetrics" : { } } ]
看完怎么用,接下來看看在項(xiàng)目中注意的點(diǎn)和值得注意的參數(shù)注解。
2.JMH的使用
引入依賴
由于這不是標(biāo)準(zhǔn)庫(kù)有的依賴,所以這里我們依然用 Maven 管理依賴,在我們構(gòu)建的 Maven 項(xiàng)目中的 pom.xml 添加下列依賴:
<dependencies> <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>
接下來看看代碼應(yīng)用。
代碼示例基于參考編寫:
package org.example; import org.openjdk.jmh.annotations.*; import org.openjdk.jmh.results.format.ResultFormatType; 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; import java.util.concurrent.TimeUnit; @BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.MILLISECONDS) @State(Scope.Thread) @Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS) @Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) @Fork(1) @Threads(2) public class MyBenchmarkTest { @Benchmark public long shift() { long t = 455565655225562L; long a = 0; for (int i = 0; i < 1000; i++) { a = t >> 30; } return a; } @Benchmark public long div() { long t = Long.MAX_VALUE; long a = 0; for (int i = 0; i < 1000; i++) { a = t / 1024 / 1024 / 1024; } return a; } public static void main(String[] args) throws RunnerException { Options opts = new OptionsBuilder() .include(MyBenchmarkTest.class.getSimpleName()) .result("MyBenchmarkTest.json") .resultFormat(ResultFormatType.JSON) .build(); new Runner(opts).run(); } }
在示例中,其實(shí)我們的目的就是測(cè)試 移位和整除 兩個(gè)方法的性能,看看每秒的吞吐量如何,最后將結(jié)果匯總在 MyBenchmarkTest.json 文件中,當(dāng)然運(yùn)行測(cè)試,我們也可以在控制臺(tái)得到相應(yīng)輸出。
注解
在上面的 demo 中,我們?cè)陬惿霞恿撕芏嘧⒔?,注解的作用又是啥呢?/p>
@BenchmarkMode
該注解用來指定基準(zhǔn)測(cè)試類型,對(duì)應(yīng) Mode 選項(xiàng),修飾類和方法,這里我們修飾類,注解的 value 是 Mode[] 類型,我們這里填入的是 Throughput ,表示整體吞吐量,即單位時(shí)間內(nèi)的調(diào)用量,查看 Mode 源碼就可以發(fā)現(xiàn),其實(shí)總的類型有以下:
Throughput: 略
AverageTime: 平均耗時(shí),指的是每次執(zhí)行的平均時(shí)間。如果這個(gè)值很小不好辨認(rèn),可以把統(tǒng)計(jì)的單位時(shí)間調(diào)小一點(diǎn)。
SampleTime: 隨機(jī)取樣。
SingleShotTime: 如果你想要測(cè)試僅僅一次的性能,比如第一次初始化花了多長(zhǎng)時(shí)間,就可以使用這個(gè)參數(shù),其實(shí)和傳統(tǒng)的main方法沒有什么區(qū)別。
All: 所有的指標(biāo),都算一遍。
從 控制臺(tái)的結(jié)果可以看看相關(guān)輸出:
Benchmark Mode Cnt Score Error Units
MyBenchmarkTest.div thrpt 5 500758.115 ± 3350.796 ops/ms
MyBenchmarkTest.shift thrpt 5 500045.811 ± 1609.779 ops/ms
如果填入 Mode.All 看看結(jié)果輸出:
Benchmark Mode Cnt Score Error Units
MyBenchmarkTest.div thrpt 5 500554.176 ± 8015.731 ops/ms
MyBenchmarkTest.shift thrpt 5 499731.423 ± 4635.160 ops/ms
MyBenchmarkTest.div avgt 5 ≈ 10?? ms/op
MyBenchmarkTest.shift avgt 5 ≈ 10?? ms/op
MyBenchmarkTest.div sample 316909 ≈ 10?? ms/op
MyBenchmarkTest.div:div·p0.00 sample ≈ 0 ms/op
MyBenchmarkTest.div:div·p0.50 sample ≈ 0 ms/op
MyBenchmarkTest.div:div·p0.90 sample ≈ 10?? ms/op
MyBenchmarkTest.div:div·p0.95 sample ≈ 10?? ms/op
MyBenchmarkTest.div:div·p0.99 sample ≈ 10?? ms/op
MyBenchmarkTest.div:div·p0.999 sample ≈ 10?? ms/op
MyBenchmarkTest.div:div·p0.9999 sample 0.002 ms/op
MyBenchmarkTest.div:div·p1.00 sample 0.025 ms/op
MyBenchmarkTest.shift sample 315964 ≈ 10?? ms/op
MyBenchmarkTest.shift:shift·p0.00 sample ≈ 0 ms/op
MyBenchmarkTest.shift:shift·p0.50 sample ≈ 0 ms/op
MyBenchmarkTest.shift:shift·p0.90 sample ≈ 10?? ms/op
MyBenchmarkTest.shift:shift·p0.95 sample ≈ 10?? ms/op
MyBenchmarkTest.shift:shift·p0.99 sample ≈ 10?? ms/op
MyBenchmarkTest.shift:shift·p0.999 sample ≈ 10?? ms/op
MyBenchmarkTest.shift:shift·p0.9999 sample 0.001 ms/op
MyBenchmarkTest.shift:shift·p1.00 sample 0.024 ms/op
MyBenchmarkTest.div ss 5 0.052 ± 0.091 ms/op
MyBenchmarkTest.shift ss 5 0.015 ± 0.023 ms/op
此時(shí)可以看到十分詳盡的輸出,每秒的吞吐量,每個(gè)操作的耗費(fèi)時(shí)間等,因?yàn)楸纠?jiǎn)單,時(shí)間耗費(fèi)建議填入 ns 等單位。
@BenchmarkMode 表示單位時(shí)間的操作數(shù)或者吞吐量,或者每個(gè)操作耗費(fèi)的時(shí)間等,注意我們都沒有限定時(shí)間單位,所以通常這個(gè)注解也會(huì)和 @OutputTimeUnit 結(jié)合使用。
@OutputTimeUnit
基準(zhǔn)測(cè)試結(jié)果的時(shí)間類型。一般選擇秒、毫秒、微秒,這里填入的是 TimeUnit 這個(gè)枚舉類型,涉及單位很多從納秒到天都有,按需選擇,最終輸出易讀的結(jié)果。
@State
@State 指定了在類中變量的作用范圍。它有三個(gè)取值。
@State 用于聲明某個(gè)類是一個(gè)“狀態(tài)”,可以用Scope 參數(shù)用來表示該狀態(tài)的共享范圍。這個(gè)注解必須加在類上,否則提示無法運(yùn)行。
Scope有如下3種值:
- Benchmark:表示變量的作用范圍是某個(gè)基準(zhǔn)測(cè)試類。
- Thread:每個(gè)線程一份副本,如果配置了Threads注解,則每個(gè)Thread都擁有一份變量,它們互不影響。
- Group:聯(lián)系上面的@Group注解,在同一個(gè)Group里,將會(huì)共享同一個(gè)變量實(shí)例。
本例中,相關(guān)變量的作用范圍是 Thread。
@Warmup
預(yù)熱,可以加在類上或者方法上,預(yù)熱只是測(cè)試數(shù)據(jù),是不作為測(cè)量結(jié)果的。
該注解一共有4個(gè)參數(shù):
- iterations 預(yù)熱階段的迭代數(shù)
- time 每次預(yù)熱時(shí)間
- timeUnit 時(shí)間單位,通常秒
- batchSize 批處理大小,指定每次操作調(diào)用幾次方法
本例中,我們加在類上,讓它迭代3次,每次1秒,時(shí)間單位秒。
@Measurement
和預(yù)熱類似,這里的注解是會(huì)影響測(cè)試結(jié)果的,它的參數(shù)和 Warmup 一樣,這里不多介紹。
本例中我們?cè)诘性O(shè)置的是5次,每次1秒。
通常 @Warmup 和 @Measurement 兩個(gè)參數(shù)會(huì)一起使用。
@Fork
表示開啟幾個(gè)進(jìn)程測(cè)試,通常我們?cè)O(shè)為1,如果數(shù)值大于1,則啟用新的進(jìn)程測(cè)試,如果設(shè)置為0,程序依然進(jìn)行,但是在用戶的 JVM 進(jìn)程上運(yùn)行。
追蹤一下JMH的源碼,發(fā)現(xiàn)每個(gè)fork進(jìn)程是單獨(dú)運(yùn)行在Proccess
進(jìn)程里的,這樣就可以做完全的環(huán)境隔離,避免交叉影響。它的輸入輸出流,通過Socket連接的模式,發(fā)送到我們的執(zhí)行終端。
如果需要更多的設(shè)置,可以看看 Fork.class 源碼,上面還有 jvm 參數(shù)設(shè)置。
@Threads
上面的注解注重開啟幾個(gè)進(jìn)程,這里就是開啟幾個(gè)線程,只有一個(gè)參數(shù) value,指定注解的value,將會(huì)開啟并行測(cè)試,如果設(shè)置的 value 過大,如 Threads.Max,則使用處理機(jī)的相同線程數(shù)。
@Benchmark
加在測(cè)試方法上,表示該方法是需要進(jìn)行基準(zhǔn)測(cè)試的,類似 JUnit5 中的 @Test 注解需要單元測(cè)試的方法一樣。
@Setup
注解的作用就是我們需要在測(cè)試之前進(jìn)行一些準(zhǔn)備工作,比如對(duì)一些數(shù)據(jù)的初始化之類的,這個(gè)也和Junit的@Before
@Teardown
在測(cè)試之后進(jìn)行一些結(jié)束工作,主要用于資源回收
開啟測(cè)試
上述的學(xué)習(xí)中主要是相關(guān)注解,這里看看具體我們?cè)趺从谩?/p>
public static void main(String[] args) throws RunnerException { Options opts = new OptionsBuilder() // 表示包含的測(cè)試類 .include(MyBenchmarkTest.class.getSimpleName()) // 最后結(jié)果輸出文件的命名 .result("MyBenchmarkTest.json") // 結(jié)果輸出什么格式,可以是json, csv, text等 .resultFormat(ResultFormatType.JSON) .build(); new Runner(opts).run(); // 運(yùn)行 }
3.JMH可視化
作為程序開發(fā)人員,看懂測(cè)試結(jié)果沒難度,測(cè)試結(jié)果文本能可視化更好。
好在我們拿到了JMH 結(jié)果后,根據(jù)文件格式,我們可以二次加工,就可以圖表化展示[2]。
JMH 支持的幾種輸出格式:
- TEXT 導(dǎo)出文本文件。
- CSV 導(dǎo)出csv格式文件。
- SCSV 導(dǎo)出scsv等格式的文件。
- JSON 導(dǎo)出成json文件。
- LATEX 導(dǎo)出到latex,一種基于ΤΕΧ的排版系統(tǒng)。
到此這篇關(guān)于Java JMH進(jìn)行基準(zhǔn)測(cè)試的使用小結(jié)的文章就介紹到這了,更多相關(guān)Java JMH基準(zhǔn)測(cè)試內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java中的HashSet、LinkedHashSet集合解析
這篇文章主要介紹了Java中的HashSet、LinkedHashSet集合解析,與HashSet不同的是,LinkedHashSet在內(nèi)部使用了一個(gè)雙向鏈表來維護(hù)元素的順序,因此它可以保持元素的插入順序,這使得LinkedHashSet在需要保持元素順序的場(chǎng)景下非常有用,需要的朋友可以參考下2023-11-11java調(diào)用shell命令并獲取執(zhí)行結(jié)果的示例
今天小編就為大家分享一篇java調(diào)用shell命令并獲取執(zhí)行結(jié)果的示例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-07-07java使用EasyExcel導(dǎo)入導(dǎo)出excel
導(dǎo)入導(dǎo)出excel數(shù)據(jù)是常見的需求,今天就來看一下Java基于EasyExcel實(shí)現(xiàn)這個(gè)功能,感興趣的朋友可以了解下2021-05-05SpringBoot?2.x整合Log4j2日志的詳細(xì)步驟
log4j2優(yōu)越的性能其原因在于log4j2使用了LMAX,一個(gè)無鎖的線程間通信庫(kù)代替了,logback和log4j之前的隊(duì)列,并發(fā)性能大大提升,下面這篇文章主要給大家介紹了關(guān)于SpringBoot?2.x整合Log4j2日志的相關(guān)資料,需要的朋友可以參考下2022-10-10解析Java的Spring框架的基本結(jié)構(gòu)
這篇文章主要介紹了Java的Spring框架的基本結(jié)構(gòu),作者從Spring的設(shè)計(jì)角度觸發(fā)解析其基礎(chǔ)的架構(gòu)內(nèi)容,需要的朋友可以參考下2016-03-03