先月末に開催されたSpringOne Platform 2018の録画がもうアップされてて、ちょこっとずつ見てってるんだけど、何日か前にDaveのセッションを見た。面白かった。
Springアプリの起動スピードについてのセッション。で、その起動スピードを自分でも確認したいなーと思ったんだけど、DaveがベンチマークのツールとしてOpenJDKのJMHというものを使ってるので、まずはそれを知らなきゃなと思って触ってみた。
## Hello JMH
JMHのサイトに書いてある通り、プロジェクトを作って
$ mvn archetype:generate \ -DinteractiveMode=false \ -DarchetypeGroupId=org.openjdk.jmh \ -DarchetypeArtifactId=jmh-java-benchmark-archetype \ -DgroupId=com.example \ -DartifactId=hello-jmh \ -Dversion=1.0
こうやったら実行できる。
$ cd hello-jmh $ mvn clean install $ java -jar target/benchmarks.jar (...) Result "com.example.MyBenchmark.testMethod": 2431425768.902 ±(99.9%) 262046906.678 ops/s [Average] (min, avg, max) = (1148300545.795, 2431425768.902, 2787377007.996), stdev = 349825123.190 CI (99.9%): [2169378862.223, 2693472675.580] (assumes normal distribution)
空っぽのメソッドのベンチマークを、デフォルトの設定で実行して取得してるみたいね。
@Benchmark public void testMethod() { // This is a demo/sample template for building your JMH benchmarks. Edit as needed. // Put your benchmark code here. }
## そもそもJMHって?
マイクロベンチマークは色んなものが影響するから難しいみたい。へー。そうなのかー。
なので、その辺りを考慮して作られてるのがJMH。
- ウォームアップ機能
- リフレクションじゃなくてAnnotation Processingを使ってコンパイル時にコードを生成
- 対象のコードをラッピングして、ループの最適化が発生しない繰り返し回数を設定
- Dead-Code Eliminationを回避するための仕組み
- などなど
## 具体的にどう使うんだろう?
と思ったので、この本のCh.5をさらーっとだけ読んで雰囲気だけ理解してから
サンプルコードを落としてきて読んだ。
各クラスのmain()
メソッドのところのコメントに起動コマンド書いてるから、時間短縮のためにそこにちょこっとパラメーターを足して実行したりした。
## 1. ベンチマークの対象について
### @Benchmark
@Benchmark public void wellHelloThere() {
@Benchmark
をつけたメソッドがベンチマークの対象になるpublic
なメソッドじゃないといけない- 引数に許されているのは
@State
アノテーションのついたクラスかJMH infrastructureのクラス(あとで出てくるBlackholeとか)のみ
### @BenchmarkMode
@Benchmark @BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.SECONDS) public void measureThroughput() throws InterruptedException {
@Benchmark
メソッドにつけてモードを指定- クラスにつけることもできて、その場合はクラス内の全部の
@Benchmark
に反映される - 実行時のオプションで上書きすることができる
- ↓こんな風に複数のモードを指定することも可能
@Benchmark @BenchmarkMode({Mode.Throughput, Mode.AverageTime, Mode.SampleTime, Mode.SingleShotTime}) @OutputTimeUnit(TimeUnit.MICROSECONDS) public void measureMultiple() throws InterruptedException {
### モード
Mode.Throughput
- 決められた時間の中で実行を繰り返して
- 単位時間あたりに何回実行できたかを計測
Mode.AverageTime
- 決められた時間の中で実行を繰り返して
- 平均実行時間を計測
Mode.SampleTime
- 決められた時間の中で実行を繰り返すが
- その中のいくつかのサンプルで実行時間を計測
Mode.SingleShotTime
- 1回だけ実行してその時間を計測
Mode.All
- 上記の全てのモードを指定したのと同じ
### @OutputTimeUnit
- 単位時間を指定
- メソッド、クラスのどちらにでも指定可能
- 実行時オプションで上書き可能
## 2. ベンチマークの状態について
### @State
@State(Scope.Benchmark) public static class BenchmarkState { volatile double x = Math.PI; } @State(Scope.Thread) public static class ThreadState { volatile double x = Math.PI; } @Benchmark public void measureUnshared(ThreadState state) { state.x++; } @Benchmark public void measureShared(BenchmarkState state) { state.x++; }
### @State
のスコープ
Scope.Benchmark
- 全てのワーカースレッドで共有される
Scope.Thread
- 全てのインスタンスは別のものになる
Scope.Group
- スレッドグループ内で共有される
### Default State
@State(Scope.Thread) public class JMHSample_04_DefaultState { double x = Math.PI; @Benchmark public void measure() { x++; }
### State Fixtures
@Setup
と@TearDown
をメソッドにつけることで、Stateの前処理や後処理をすることができる- FixtureメソッドにはLevelを指定することができる
## 3. Blackhole
について
### Dead-Code Elimination
private double x = Math.PI; @Benchmark public void measureWrong() { // これだと結果が利用されないので削除対象になる Math.log(x); } @Benchmark public double measureRight() { // これで結果が利用されるので大丈夫 return Math.log(x); }
結果はこうなった。baselineは何もしてないメソッド。面白い。
Benchmark Mode Cnt Score Error Units JMHSample_08_DeadCode.baseline avgt 5 0.357 ± 0.033 ns/op JMHSample_08_DeadCode.measureRight avgt 5 25.322 ± 0.873 ns/op JMHSample_08_DeadCode.measureWrong avgt 5 0.352 ± 0.010 ns/op
### 複数対応
これだとMath.log(x1);
の方は削除対象になる
@Benchmark public double measureWrong() { Math.log(x1); return Math.log(x2); }
のでこうするか
@Benchmark public double measureRight_1() { return Math.log(x1) + Math.log(x2); }
Blackhole
を引数に渡してこうする
@Benchmark public void measureRight_2(Blackhole bh) { bh.consume(Math.log(x1)); bh.consume(Math.log(x2)); }
### Constant Folding
- 結果が同じ値になることが分かってる場合にConstant Foldingで最適化されて畳み込まれてしまうことを防ぐ
- そのためには
@State
の非finalフィールドを使用する
@State(Scope.Thread) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) public class JMHSample_10_ConstantFold { private double x = Math.PI; private final double wrongX = Math.PI; @Benchmark public double measureWrong_1() { // これだと畳み込めるからダメ。 return Math.log(Math.PI); } @Benchmark public double measureWrong_2() { // これも畳み込めるからダメ。 return Math.log(wrongX); } @Benchmark public double measureRight() { // これだと予測可能にならないからOK return Math.log(x); }
結果はこうなった。面白いー。
Benchmark Mode Cnt Score Error Units JMHSample_10_ConstantFold.baseline avgt 5 2.693 ± 1.774 ns/op JMHSample_10_ConstantFold.measureRight avgt 5 25.476 ± 1.170 ns/op JMHSample_10_ConstantFold.measureWrong_1 avgt 5 2.662 ± 1.867 ns/op JMHSample_10_ConstantFold.measureWrong_2 avgt 5 2.684 ± 1.870 ns/op
ここまででサンプルの1から10までをざっくりカバーしたくらいかな。サンプルは38まであるから、また気が向いたときに続きやるかもー。
2018-10-30追記:後編書いた