Java函數(shù)性能分析
測(cè)試函數(shù)性能,比較兩個(gè)函數(shù)的執(zhí)行效率差異是開發(fā)時(shí)經(jīng)常面臨的場(chǎng)景,像Go官方提供了 benchmark 工具,那么Java呢?
Java也提供了一款官方的微基準(zhǔn)測(cè)試工具: JMH !
-
JMH 怎么使用?
-
引入
maven依賴<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> <scope>provided</scope> </dependency> 函數(shù)添加注解:
@Benchmark-
函數(shù)或所在類添加注解:
-
@BenchmarkMode用來(lái)配置 Mode 選項(xiàng)-
Throughput:整體吞吐量,每秒執(zhí)行了多少次調(diào)用,單位為ops/time -
AverageTime:用的平均時(shí)間,每次操作的平均時(shí)間,單位為time/op -
SampleTime:隨機(jī)取樣,最后輸出取樣結(jié)果的分布 -
SingleShotTime:只運(yùn)行一次,往往同時(shí)把 Warmup 次數(shù)設(shè)為 0,用于測(cè)試?yán)鋯?dòng)時(shí)的性能 -
All:上面的所有模式都執(zhí)行一次
-
-
@Warmup預(yù)熱所需要配置的一些基本測(cè)試參數(shù)-
iterations:預(yù)熱的次數(shù) -
time:每次預(yù)熱的時(shí)間 -
timeUnit:時(shí)間的單位,默認(rèn)秒 -
batchSize:批處理大小,每次操作調(diào)用幾次方法
-
@Measurement實(shí)際調(diào)用方法所需要配置的一些基本測(cè)試參數(shù)@Threads每個(gè)進(jìn)程中的測(cè)試線程@Fork進(jìn)行 fork 的次數(shù)。如果 fork 數(shù)是 2 的話,則 JMH 會(huì) fork 出兩個(gè)進(jìn)程來(lái)進(jìn)行測(cè)試。-
@State可以指定一個(gè)對(duì)象的作用范圍。JMH 根據(jù) scope 來(lái)進(jìn)行實(shí)例化和共享操作。-
Scope.Benchmark:所有測(cè)試線程共享一個(gè)實(shí)例,測(cè)試有狀態(tài)實(shí)例在多線程共享下的性能 -
Scope.Group:同一個(gè)線程在同一個(gè) group 里共享實(shí)例 -
Scope.Thread:默認(rèn)的 State,每個(gè)測(cè)試線程分配一個(gè)實(shí)例
-
@OutputTimeUnit為統(tǒng)計(jì)結(jié)果的時(shí)間單位@Param指定某項(xiàng)參數(shù)的多種情況,特別適合用來(lái)測(cè)試一個(gè)函數(shù)在不同的參數(shù)輸入的情況下的性能,只能作用在字段上,使用該注解必須定義 @State 注解。
-
-
-
簡(jiǎn)單的代碼樣例參考?
package org.example; import org.openjdk.jmh.annotations.*; 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.AverageTime) @State(Scope.Thread) @Fork(1) @OutputTimeUnit(TimeUnit.MILLISECONDS) @Warmup(iterations = 3) @Measurement(iterations = 5) public class JmhTest { String string = ""; StringBuilder stringBuilder = new StringBuilder(); @Benchmark public String stringAdd() { for (int i = 0; i < 1000; i++) { string = string + i; } return string; } @Benchmark public String stringBuilderAppend() { for (int i = 0; i < 1000; i++) { stringBuilder.append(i); } return stringBuilder.toString(); } public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder() .include(JmhTest.class.getSimpleName()) .build(); new Runner(opt).run(); } }
使用JMH時(shí)需要注意
測(cè)試代碼注意避免JVM的優(yōu)化,導(dǎo)致JMH分析結(jié)果對(duì)你出現(xiàn)誤判。JMH運(yùn)行統(tǒng)計(jì)是基于JVM運(yùn)行,你的代碼在JVM運(yùn)行前會(huì)被JIT優(yōu)化。包括但不限于:
- 死碼消除
@Benchmark public void testStringAdd(Blackhole blackhole) { String a = ""; for (int i = 0; i < length; i++) { a += i; } } // JVM 可能會(huì)認(rèn)為變量 a 從來(lái)沒有使用過(guò),從而進(jìn)行優(yōu)化把整個(gè)方法內(nèi)部代碼移除掉,這就會(huì)影響測(cè)試結(jié)果。 // MH 提供了兩種方式避免這種問題,一種是將這個(gè)變量作為方法返回值 return a,一種是通過(guò) Blackhole 的 consume 來(lái)避免 JIT 的優(yōu)化消除。 - 常量折疊
- 常量傳播
- .....(更多的陷阱demo參考: https://github.com/lexburner/JMH-samples)