これの続き
## 実行時オプション
サンプルを実行しながらなんとなくそんな気がしてたけど
❯ java -jar target/benchmarks.jar JMHSample_11 -f 1 -w 3 -wi 1 -r 3 -i 5
これだと
JMHSample_11
を含むベンチーマークを- 1個のフォークで(
-f 1
) - ウォームアップイテレーションは3秒(
-w 3
)を1回(-wi 1
) - 計測用のイテレーションは3秒(
-r 3
)を5回(-i 5
)
という意味になる。-h
をつけることでオプションの一覧を見ることができる。
## ループの最適化の影響に注意する
自分でループを使うんじゃなくてJMHの計測の繰り返し処理に任せる。ループを使うと最適化によってアンロールされてパイプライン化されたりしてしまうから。
Benchmark Mode Cnt Score Error Units JMHSample_11_Loops.measureRight avgt 5 2.262 ± 0.333 ns/op JMHSample_11_Loops.measureWrong_1 avgt 5 2.657 ± 1.407 ns/op JMHSample_11_Loops.measureWrong_10 avgt 5 0.315 ± 0.135 ns/op JMHSample_11_Loops.measureWrong_100 avgt 5 0.031 ± 0.012 ns/op JMHSample_11_Loops.measureWrong_1000 avgt 5 0.028 ± 0.021 ns/op JMHSample_11_Loops.measureWrong_10000 avgt 5 0.021 ± 0.002 ns/op JMHSample_11_Loops.measureWrong_100000 avgt 5 0.019 ± 0.003 ns/op
へー。ループ10回するだけで、もう1回あたりの平均実行時間は、ずれるんだねぇ。
どうしてもループを使わなきゃいけない場合は、Blackholeに吸わせることで、この最適化を避けることができる。
@Benchmark public void measureRight_1(Blackhole bh) { for (int x : xs) { bh.consume(work(x)); } }
試しにJMHSample_11_Loops
をBlackholeに吸わせてJMHSample_11_Loops2
を作ってみたらこんな感じになった。 へー。良くなった。
❯ java -jar target/benchmarks.jar JMHSample_11 -f 1 -wi 1 -r 3 -i 3 -w 3 ... Benchmark Mode Cnt Score Error Units JMHSample_11_Loops.measureRight avgt 3 2.940 ± 1.462 ns/op JMHSample_11_Loops.measureWrong_1 avgt 3 2.911 ± 2.070 ns/op JMHSample_11_Loops.measureWrong_10 avgt 3 0.372 ± 0.523 ns/op JMHSample_11_Loops.measureWrong_100 avgt 3 0.042 ± 0.021 ns/op JMHSample_11_Loops.measureWrong_1000 avgt 3 0.039 ± 0.085 ns/op JMHSample_11_Loops.measureWrong_10000 avgt 3 0.030 ± 0.025 ns/op JMHSample_11_Loops.measureWrong_100000 avgt 3 0.027 ± 0.005 ns/op JMHSample_11_Loops2.measureRight avgt 3 3.240 ± 5.628 ns/op JMHSample_11_Loops2.measureWrong_1 avgt 3 3.234 ± 4.986 ns/op JMHSample_11_Loops2.measureWrong_10 avgt 3 2.966 ± 2.372 ns/op JMHSample_11_Loops2.measureWrong_100 avgt 3 3.013 ± 3.742 ns/op JMHSample_11_Loops2.measureWrong_1000 avgt 3 3.003 ± 3.001 ns/op JMHSample_11_Loops2.measureWrong_10000 avgt 3 2.838 ± 0.355 ns/op JMHSample_11_Loops2.measureWrong_100000 avgt 3 2.853 ± 1.185 ns/op
## プロファイルに基づく最適化の影響に注意する
Profile-Guided Optimizations(プロファイルに基づく最適化)の影響を避けるために、フォークして別プロセスで実行する。デフォルトでは5つのフォークが実行されるようになっている。
・・・なるほど?(←わかってない)よく分かってないけど、同じプロセスだと最適化の影響があるっぽいことだけ分かった。
@Fork
アノテーションを使ってフォーク数を指定できる。↓だと1個
@Benchmark @Fork(1) public int measure_4_forked_c1() { return measure(c1); }
↓0だとフォークなしで、これは良くない(警告が出る)。
@Benchmark @Fork(0) public int measure_1_c1() { return measure(c1); }
フォーク数を指定すると、その数だけ実行されるので実行ごとの分散を評価することができる。
@Benchmark @Fork(20) public void fork_1(SleepyState s) throws InterruptedException { TimeUnit.MILLISECONDS.sleep(s.sleepTime); }
@Fork
を指定しなかったらデフォルトは5。
## グルーピング
@Group
を使うと、複数のベンチマークメソッドをグルーピングして、それぞれのメソッドが使用するスレッドの割合を調整して実行することができる。あぁ、これ用にScope.Group
があるのか(前回の記事参照)。
❯ java -jar target/benchmarks.jar JMHSample_15 -f 1 ... # Threads: 4 threads (1 group; 1x "get", 3x "inc" in each group), will synchronize iterations ... Benchmark Mode Cnt Score Error Units JMHSample_15_Asymmetric.g avgt 5 53.744 ± 12.789 ns/op JMHSample_15_Asymmetric.g:get avgt 5 28.416 ± 10.066 ns/op JMHSample_15_Asymmetric.g:inc avgt 5 62.187 ± 15.156 ns/op
## SyncIterationオプション(-si
)
複数のスレッドで実行するときにスレッドの開始終了部分がパフォーマンスに影響するのを避けるために、立ち上げと終了を除去する仕組み。デフォルトでtrue
になってる。
trueの場合
❯ java -jar target/benchmarks.jar JMHSample_17 \ -w 1s -r 1s -f 1 -t 64 -si true ... Benchmark Mode Cnt Score Error Units JMHSample_17_SyncIterations.test thrpt 5 134.461 ± 7.350 ops/ms
falseの場合
❯ java -jar target/benchmarks.jar JMHSample_17 \ -w 1s -r 1s -f 1 -t 64 -si false ... Benchmark Mode Cnt Score Error Units JMHSample_17_SyncIterations.test thrpt 5 143.721 ± 5.549 ops/ms
起動終了部分の影響でスコアは良くなるけど、この結果は、あてにならない。
## アノテーションで指定
WarmupやMeasurementの回数や時間などはアノテーションで指定可能。クラスでもメソッドでもつけられる。
@Warmup(iterations = 5, time = 100, timeUnit = TimeUnit.MILLISECONDS) @Measurement(iterations = 5, time = 100, timeUnit = TimeUnit.MILLISECONDS)
## 他にも
- BlackholeにCPUを消費させることができたり
- 複数スレッドがデータを扱う際の、FalseSharingによるパフォーマンス劣化に対する対応をStateが最初からしてくれてたり
- 一定時間内に実行できた回数でパフォーマンスをはかるより、計測回数を指定してパフォーマンスを図った方が良い場合はシングルショットxバッチサイズでやることができる
- 設定ごとのパフォーマンスの違いを見たい場合は
@Param
アノテーションでパラメータを指定する。パラメータの組み合わせもやってくれるっぽい
とか、色々ある。面白かったー!!
## 試しに
あんまり考えずに、こういうコードを書いてみた。
@Fork(1) @Warmup(iterations=1, batchSize = 10000) @Measurement(iterations=5, batchSize = 10000) @BenchmarkMode(Mode.SingleShotTime) @OutputTimeUnit(TimeUnit.MILLISECONDS) @State(Scope.Thread) public class MyBenchmark { String s = ""; StringBuilder sb = new StringBuilder(); String word = "a"; @Benchmark public String baseline() { return s; } @Benchmark public String measureStringPlus() { s += word; return s; } @Benchmark public StringBuilder measureStringBuilderAppend() { sb.append(word); return sb; } }
実行してみたらこうなった。
❯ java -jar target/benchmarks.jar MyBenchmark # JMH version: 1.21 # VM version: JDK 1.8.0_191, Java HotSpot(TM) 64-Bit Server VM, 25.191-b12 ... Benchmark Mode Cnt Score Error Units MyBenchmark.baseline ss 5 0.430 ± 0.243 ms/op MyBenchmark.measureStringBuilderAppend ss 5 0.697 ± 0.317 ms/op MyBenchmark.measureStringPlus ss 5 147.748 ± 246.386 ms/op
へー。200倍くらい違う。
なんとなく、Java 11に変えてみる。
❯ sdk use java 11.0.1-open Using java version 11.0.1-open in this shell. ❯ java -jar target/benchmarks.jar MyBenchmark ... # JMH version: 1.21 # VM version: JDK 11.0.1, OpenJDK 64-Bit Server VM, 11.0.1+13 ... Benchmark Mode Cnt Score Error Units MyBenchmark.baseline ss 5 0.752 ± 0.017 ms/op MyBenchmark.measureStringBuilderAppend ss 5 0.834 ± 0.641 ms/op MyBenchmark.measureStringPlus ss 5 75.690 ± 171.308 ms/op
お。+
がJava 8のときより速くなった。
んー。なんとなくEpsilonGC(何もしないGC)をONにしてみる
❯ java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC -jar target/benchmarks.jar MyBenchmark ... Benchmark Mode Cnt Score Error Units MyBenchmark.baseline ss 5 0.970 ± 2.173 ms/op MyBenchmark.measureStringBuilderAppend ss 5 0.641 ± 0.911 ms/op MyBenchmark.measureStringPlus ss 5 161.040 ± 302.442 ms/op
あれ。速くなるのかなぁと思ってたら、Java 8のときより遅くなっちゃった。ふむー。面白い。それぞれで何がどう影響してこういう結果になってるのか全然分かんない。もっと勉強しなきゃなぁ。マイクロベンチマークって難しいな。
## ところで、何でJMHチェックし始めたんだっけ?
と思ったら、そうだ、S1Pのセッションを見てたんだった。次はそれ見てみよっと。意味が分かるようになってるといいな。