JMH: 最裝逼,最牛逼的基準測試工具套件

JMH簡介

官網:http://openjdk.java.net/projects/code-tools/jmh/

簡介:JMH is a Java harness for building, running, and analysing nano/micro/milli/macro benchmarks written in Java and other languages targetting the JVM,由簡介可知,JMH不止能對Java語言做基準測試,還能對運行在JVM上的其他語言做基準測試。而且可以分析到納秒級別。

推薦用法

官方推薦創建一個獨立的Maven工程來運行JMH基準測試,這樣更能確保結果的準確性。當然也可以在已存在的工程中,或者在IDE上運行,但是越復雜,結果越不可靠(more complex and the results are less reliable)。

簡單實用

推薦用法通過命令行創建,構建和運行JMH基準測試。

setup

生成一個新的JMH工程的maven命令如下:

 mvn archetype:generate 
 -DinteractiveMode=false 
 -DarchetypeGroupId=org.openjdk.jmh 
 -DarchetypeArtifactId=jmh-java-benchmark-archetype 
 -DgroupId=com.afei.jmh 
 -DartifactId=jmh 
 -Dversion=1.0.0-SNAPSHOT

執行該命令后,會創建一個Maven工程,但是默認生成的MyBenchmark.java并沒有在預期的包名com/afei/jmh中,即使加上參數-DpackageName=com.afei.jmh也不行,只能先手動將其挪到包名下,這里作為一個小小的遺留問題。

壓測代碼

默認生成的MyBenchmark.java源碼如下,testMethod()中就是你要壓測的代碼,下面是筆者要壓測的洗牌算法:

@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(1)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class MyBenchmark {

    @GenerateMicroBenchmark
    public List<Integer> testMethod() {
        int cardCount = 54;
        List<Integer> cardList = new ArrayList<Integer>();
        for (int i=0; i<cardCount; i++){
            cardList.add(i);
        }
        // 洗牌算法
        Random random = new Random();
        for (int i=0; i<cardCount; i++) {
            int rand = random.nextInt(cardCount);
            Collections.swap(cardList, i, rand);
        }
        return cardList;
    }

}

build

寫完代碼接下來就是構建并打包,在pom.xml所在目錄執行如下命令:

mvn clean package

說明:這一步,也可以通過IDE工具構建打包。

running

打包成功后在target目錄下生成了一個JAR文件:microbenchmarks.jar,需要注意的是,官網的運行命令是java -jar target/benchmarks.jar,至于到底是benchmarks.jar還是microbenchmarks.jar,取決于你的POM文件:

<configuration>
    <finalName>microbenchmarks</finalName>
    <transformers>
        <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
            <mainClass>org.openjdk.jmh.Main</mainClass>
        </transformer>
    </transformers>
</configuration>

筆者生成的maven工程是microbenchmarks,所以,運行時執行如下命令:

java -jar target/microbenchmarks.jar

輸出結果如下:

# Run progress: 0.00% complete, ETA 00:00:10
# VM invoker: C:\Program Files\Java\jre1.8.0_181\bin\java.exe
# VM options: <none>
# Fork: 1 of 1
# Warmup: 5 iterations, 1 s each
# Measurement: 5 iterations, 1 s each
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: com.afei.jmh.MyBenchmark.testMethod
# Warmup Iteration   1: 1133.738 ns/op
# Warmup Iteration   2: 1169.750 ns/op
# Warmup Iteration   3: 1066.204 ns/op
# Warmup Iteration   4: 1086.300 ns/op
# Warmup Iteration   5: 1145.228 ns/op
Iteration   1: 1045.157 ns/op
Iteration   2: 1064.303 ns/op
Iteration   3: 1064.227 ns/op
Iteration   4: 1053.979 ns/op
Iteration   5: 1055.718 ns/op

Result : 1056.677  ±(99.9%) 30.809 ns/op
  Statistics: (min, avg, max) = (1045.157, 1056.677, 1064.303), stdev = 8.001
  Confidence interval (99.9%): [1025.868, 1087.486]

Benchmark                        Mode   Samples         Mean   Mean error    Units
c.a.j.MyBenchmark.testMethod     avgt         5     1056.677       30.809    ns/op

結果解讀

下面對輸出結果一些重要信息進行解讀:

@Warmup

由于筆者加了這個注解:@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)。所以,基準測試后對代碼預熱總計5秒(迭代5次,每次1秒)。預熱對于壓測來說非常非常重要,如果沒有預熱過程,壓測結果會很不準確。這個注解對應的日志如下:

# Warmup Iteration   1: 1133.738 ns/op
# Warmup Iteration   2: 1169.750 ns/op
# Warmup Iteration   3: 1066.204 ns/op
# Warmup Iteration   4: 1086.300 ns/op
# Warmup Iteration   5: 1145.228 ns/op

@Measurement

另外一個重要的注解:@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS),表示循環運行5次,總計5秒時間。

@Fork

這個注解表示fork多少個線程運行基準測試,如果@Fork(1),那么就是一個線程,這時候就是同步模式。

@BenchmarkMode&@OutputTimeUnit

基準測試模式申明為:@BenchmarkMode(Mode.AverageTime)搭配@OutputTimeUnit(TimeUnit.NANOSECONDS)(可選基準測試模式通過枚舉Mode得到),筆者的示例是AverageTime,即表示每次操作需要的平均時間,而OutputTimeUnit申明為納秒,所以基準測試單位是ns/op,即每次操作的納秒單位平均時間。基準測試結果如下:

Result : 1056.677 ±(99.9%) 30.809 ns/op
  Statistics: (min, avg, max) = (1045.157, 1056.677, 1064.303), stdev = 8.001
  Confidence interval (99.9%): [1025.868, 1087.486]

最后一段結果如下,重點關注MeanUnits兩個字段,組合起來就是1227.928ns/op,即每次操作耗時1056.677納秒:

Benchmark                        Mode   Samples         Mean   Mean error    Units
c.a.j.MyBenchmark.testMethod     avgt         5     1056.677       30.809    ns/op

如果我們將@BenchmarkMode(Mode.AverageTime)@OutputTimeUnit(TimeUnit.NANOSECONDS)的組合,改成@BenchmarkMode(Mode.Throughput)@OutputTimeUnit(TimeUnit.MILLISECONDS),那么基準測試結果就是每毫秒的吞吐量(即每毫秒多少次操作),結果如下,表示943.437ops/ms:

Benchmark                        Mode   Samples         Mean   Mean error    Units
c.a.j.MyBenchmark.testMethod    thrpt         5      943.437       44.060   ops/ms

Mean error表示誤差,或者波動,與Result的±值對應:Result : 1056.677 ±(99.9%) 30.809 ns/op

基準測試對比

將自定義洗牌算法和JDK原生的洗牌算法Collections.shuffle(cardList);進行基準測試對比,結果如下:

- ops/ms ns/op
JDK原生洗牌算法 807.470 1149.900
自定義洗牌算法(for循環外面new Random) 943.437 1056.677
自定義洗牌算法(for循環里面new Random) 300.467 3346.509

Random在for循環里面的源碼如下:

for (int i=0; i<cardCount; i++) {
    Random random = new Random();
    int rand = random.nextInt(cardCount);
    Collections.swap(cardList, i, rand);
}

說明,自定義洗牌算法事實上就是JDK自帶洗牌算法中集合的size少于SHUFFLE_THRESHOLD(這個值為5)時的實現。另外,由基準測試對比可知,for循環里面不斷new Random的性能相比只在for循環外面new Random一次的性能要差好3倍左右。

另外,在集合的size超過SHUFFLE_THRESHOLD即5后JDK原生洗牌算法,相比size少于該值得洗牌算法性能并沒有提高,兩者性能差在10%左右,基本可以忽略。這里想不明白,JDK原生洗牌算法在集合的size超過SHUFFLE_THRESHOLD的優化的意思,當然也可能跟筆者基準測試樣本有關(畢竟筆者只測試了size為54的集合)。

JMH和jMeter的不同

JMH和jMeter的使用場景還是有很大的不同的,jMeter更多的是對rest api進行壓測,而JMH關注的粒度更細,它更多的是發現某塊性能槽點代碼,然后對優化方案進行基準測試對比。比如json序列化方案對比,bean copy方案對比,文中提高的洗牌算法對比等。

案例參考

官方給了很多樣例代碼,有興趣的同學可以自己查詢并學習JMH:http://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,886評論 18 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,124評論 25 708
  • 1.炒股 不能追漲殺跌,一追漲就失去先手,買入成本價就比較高,沒有成本優勢。 一般來說追高買入,買入價會比較高,而...
    shanshukeng閱讀 369評論 0 0
  • 困啦,睡吧,又玩一晚上,花了28 大餐額 最得意的說 永紅要走了,他們都要支教啦, 對了我居然問報教師資格證的人要...
    宋長金j閱讀 92評論 0 0
  • 有些人活成了自己生命的旁觀者。 成長的路上有一條彎路,叫活成了自己生命的旁觀者。 學習相同的心理學課程,有人飛快的...
    梓茗森林閱讀 294評論 0 0