JMH(Java Microbenchmark Harness)のサンプルを動かしながら勉強

先月末に開催されたSpringOne Platform 2018の録画がもうアップされてて、ちょこっとずつ見てってるんだけど、何日か前にDaveのセッションを見た。面白かった。

How Fast is Spring? - YouTube

Springアプリの起動スピードについてのセッション。で、その起動スピードを自分でも確認したいなーと思ったんだけど、DaveがベンチマークのツールとしてOpenJDKのJMHというものを使ってるので、まずはそれを知らなきゃなと思って触ってみた。

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って?

マイクロベンチマークは色んなものが影響するから難しいみたい。へー。そうなのかー。

  • ウォームアップ
    • 一定回数以上呼び出されたらJIT(Just-in-Time)コンパイラーがバイトコードをマシンコードに変換するから、それを考慮したウォームアップが必要
  • GC(Garbage Collection)の影響
  • JITコンパイラーのDead-Code Eliminationなどの最適化の影響
  • などなど

なので、その辺りを考慮して作られてるのがJMH。

  • ウォームアップ機能
  • リフレクションじゃなくてAnnotation Processingを使ってコンパイル時にコードを生成
  • 対象のコードをラッピングして、ループの最適化が発生しない繰り返し回数を設定
  • Dead-Code Eliminationを回避するための仕組み
  • などなど

## 具体的にどう使うんだろう?

と思ったので、この本のCh.5をさらーっとだけ読んで雰囲気だけ理解してから

www.safaribooksonline.com

サンプルコードを落としてきて読んだ。

http://hg.openjdk.java.net/code-tools/jmh/file/66fb723292d4/jmh-samples/src/main/java/org/openjdk/jmh/samples

各クラスの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

  • クラスにつける
  • ベンチマークが実行されている状態をカプセル化
  • 通常は@Benchmarkメソッドに引数として渡される
  • その初期化と共有はJMHが面倒をみる
    @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

  • ベンチマーク対象のコードが、コンパイラーのDead-Code Elimination対象にならないようにする
  • メソッドから結果を返すことでJMHのBlackholeがそれを使うので削除対象外になる
    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追記:後編書いた

bufferings.hatenablog.com