JMH 筆記


Nolan Issac

使用 JMH 进行微基准测试:不要猜,要测试!

這幾天讀了幾篇很有趣的文章, 是關於 lambda 跟 jvm 效能評估的文章,
分別是
Java8 Lambda表达式和流操作如何让你的代码变慢5倍
使用JMH进行微基准测试:不要猜,要测试!


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 是用來衡量 JVM 容器上運作的
(Java, Scala, Kotlin, Groovy, Clojure, etc.) 效能的工具,
官方建議透過 maven 來建立測試專案, 可以避免一些奇怪設定影響效能的問題,
groupdId 就替換成自己的 package name 吧,
artifactId 就替換成測試的 project name, 會按照這個 project name 在當下路徑建立一個資料夾,


mvn archetype:generate \
          -DinteractiveMode=false \
          -DarchetypeGroupId=org.openjdk.jmh \
          -DarchetypeArtifactId=jmh-java-benchmark-archetype \
          -DgroupId=com.example \
          -DartifactId=jmh-benchmark \
          -Dversion=1.0

maven Builde 出來的 code

import org.openjdk.jmh.annotations.Benchmark;

public class MyBenchmark {

    @Benchmark
    public void testMethod() {
        // This is a demo/sample template for building your JMH benchmarks. Edit as needed.
        // Put your benchmark code here.
    }

}

不管 3721 跑了再說,


$ cd jmh-benchmark
$ mvn clean install
$ java -jar target/benchmarks.jar

運行結果


# JMH version: 1.19
# VM version: JDK 1.8.0_92, VM 25.92-b14
# VM invoker: /Library/Java/JavaVirtualMachines/jdk1.8.0_92.jdk/Contents/Home/jre/bin/java
# VM options: 
# Warmup: 20 iterations, 1 s each
# Measurement: 20 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: com.example.MyBenchmark.testMethod

...

# Run progress: 90.00% complete, ETA 00:00:40
# Fork: 10 of 10
# Warmup Iteration   1: 3189934685.250 ops/s
# Warmup Iteration   2: 3068794073.424 ops/s
# Warmup Iteration   3: 3236114041.508 ops/s
# Warmup Iteration   4: 3134197602.346 ops/s
# Warmup Iteration   5: 3148638128.516 ops/s
# Warmup Iteration   6: 3173960761.557 ops/s
# Warmup Iteration   7: 3169265377.660 ops/s
# Warmup Iteration   8: 3077919609.443 ops/s
# Warmup Iteration   9: 3166464153.044 ops/s
# Warmup Iteration  10: 3078477796.372 ops/s
# Warmup Iteration  11: 3099929982.724 ops/s
# Warmup Iteration  12: 3036217732.999 ops/s
# Warmup Iteration  13: 3006113218.527 ops/s
# Warmup Iteration  14: 3181689542.757 ops/s
# Warmup Iteration  15: 3119538331.842 ops/s
# Warmup Iteration  16: 3105301506.039 ops/s
# Warmup Iteration  17: 3048849508.645 ops/s
# Warmup Iteration  18: 3072101635.390 ops/s
# Warmup Iteration  19: 3144030955.986 ops/s
# Warmup Iteration  20: 3151332956.538 ops/s
Iteration   1: 3164452829.947 ops/s
Iteration   2: 3113814464.802 ops/s
Iteration   3: 3052097465.786 ops/s
Iteration   4: 2980582092.738 ops/s
Iteration   5: 3042852421.053 ops/s
Iteration   6: 3091791507.999 ops/s
Iteration   7: 3137421146.240 ops/s
Iteration   8: 2921380582.360 ops/s
Iteration   9: 2991112676.148 ops/s
Iteration  10: 3141795516.024 ops/s
Iteration  11: 3106155855.084 ops/s
Iteration  12: 3099071004.994 ops/s
Iteration  13: 3165802127.937 ops/s
Iteration  14: 3117700695.037 ops/s
Iteration  15: 3184560049.227 ops/s
Iteration  16: 3176507506.414 ops/s
Iteration  17: 3133869785.413 ops/s
Iteration  18: 3150455744.882 ops/s
Iteration  19: 3189445594.500 ops/s
Iteration  20: 3089706559.098 ops/s


Result "com.example.MyBenchmark.testMethod":
  3133357710.097 ±(99.9%) 12882845.404 ops/s [Average]
  (min, avg, max) = (2921380582.360, 3133357710.097, 3284444409.646), stdev = 54546774.121
  CI (99.9%): [3120474864.693, 3146240555.501] (assumes normal distribution)


# Run complete. Total time: 00:06:44

Benchmark                Mode  Cnt           Score          Error  Units
MyBenchmark.testMethod  thrpt  200  3133357710.097 ± 12882845.404  ops/s

說明

我沒有找到很明確的說明文件, 下面的說明是從網路上整理來跟一部份自己猜測的

  • 運行的時候應盡量關閉不必要的 applications , 確保沒有其他變因。
  • 運行的次數越多越好, 避免 max / min 影響, 結果是取平均值。
  • 最上方的說明, Warmup: 20 iterations, 1 s each 預熱 20 次, 每次執行 1s, 不是很確定預熱的原理跟用意, 猜測是減少啟動 JVM 所造成的變因?
  • 最上方的說明, Measurement: 20 iterations, 1 s each 評估 20 次, 每次執行 1s。
  • 評估的單位是 ops/s (operations per second), 每秒運行有 @Benchmark 標記的 method 的次數。
  • Benchmark mode: Throughput, ops/time Throughput (吞吐量), 每秒可以運作的次數作為衡量標準, 其他 mode 下面補充。
  • 最下方的 Result, 3133357710.097 ±(99.9%) 12882845.404 ops/s, 平均是 3133357710.097 ops/s , 誤差上下 12882845.404 ops/s。
  • (min, avg, max) = (2921380582.360, 3133357710.097, 3284444409.646), stdev = 54546774.121, stdev 樣本標準差 54546774.121。
  • CI (99.9%): [3120474864.693, 3146240555.501], 常態分佈(高斯分佈) !? 數學不好, 不太確定。

Mode

  • Mode.Throughput - 評估時間內吞吐量, 單位時間內的執行次數。
  • Mode.AverageTime - 評估執行平均時間, 多次執行花費的時間, 取平均值。
  • Mode.SampleTime - 評估樣本(Sample) 的執行時間, (n %) Sample 在某個時間內執行完成
  • Mode.SingleShotTime - 冷測試評估, 不做 JVM warm up, 只執行一次有 @Benchmark 標記的 method, 用來評估在 JVM 啟動運行所需的時間。

Dead Code Elimination

Dead Code 指的是沒有被使用的 code, 比如下方的 int sum = a + b;,
sum 沒有繼續做任何運算處理, jmh 會因為 sum 沒有被使用,
而忽略評估 a + b 這段運算, 所以評估就不準啦。


    @Benchmark @BenchmarkMode(Mode.Throughput)
    public void testMethod() {
        // This is a demo/sample template for building your JMH benchmarks. Edit as needed.
        // Put your benchmark code here.

        int a = 1;
        int b = 2;
        int sum = a + b;
    }

Avoiding Dead Code Elimination

解決 Dead Code 的方法,

  1. return sum, 讓 sum 確實有被使用。
  2. Passing Value to a Black Hole , 意思是弄一個黑洞把變數丟進去, 假裝黑洞用了那個變數, 類似 Mockito 的 @Mock, 做法看下來

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.infra.Blackhole;

public class MyBenchmark {

    @Benchmark
    public void testMethod(Blackhole blackhole) {
        int a = 1;
        int b = 2;
        int sum = a + b;
        blackhole.consume(sum);
    }
}

Return the result of your code from the benchmark method.
Pass the calculated value into a "black hole" provided by JMH.

大大們的筆記

  1. tutorials.jenkov.com, 滿詳細的一篇, http://tutorials.jenkov.com/java-performance/jmh.html#your-first-jmh-benchmark
  2. java-performance.info - http://java-performance.info/jmh/
  3. importnew - http://www.importnew.com/12548.html
  4. blog.dyngr.com - http://blog.dyngr.com/blog/2016/10/29/introduction-of-jmh/

沒有留言:

張貼留言