Javaでカジュアルにマイクロベンチする際のTips

4 min read読了の目安(約4100字

はじめに

Javaでマイクロベンチマークを取るときにはJMH (Java Microbenchmark Harness)を使います。ただこのJMHは使い方を理解するまでにちょっと時間がかかるのでそれを助けるかもしれないTips(コツ)を記録しておきます。

注意: 各Tipsはテスト内容や求められる結果の厳密さ次第では無駄になるどころか有害になる可能性があります。あくまでもカジュアルな計測ではこんな使い方をすれば良さそうだな、という体で読んでください。

なお実行環境にはOpenJDK 11 + Gradle 6.7という組み合わせを前提としています。

Tips

GradleのJMHプラグインの有効化

GradleでJMHプラグインを使うには以下を書き足す必要があります。pluginsブロックが既にあるならそこに1行追加するだけです。

plugins {
    id 'me.champeau.gradle.jmh' version '0.5.2'
}

repositoriesjcenter()が必要らしいですが、だいたい入ってるでしょうからその例は省略します。

JMHプラグインでよく使いそうなタスク

これだけは覚えてきましょう。

  • jmh
    プロジェクト内のすべてのマイクロベンチマークを実行する。build/reports/jmh/results.txtに結果が出力される
  • compileJmhJava
    マイクロベンチマークのコードをコンパイルする

JMHとGoのマイクロベンチマークの考え方の違い

Goではベンチマークを書けば、それを何回実行するかどのくらいの時間動かし続けるかは明示的に指定しない限り、go testが良いように決めてくれます。JMHはそうではありません。またJMHプラグインのデフォルト設定も厄介です。

なのでJMHでマイクロベンチマークをする場合はテストケース毎に、何を計測するのか何回実行するかどのくらいの期間実行するのか、を決めてアノテーションで指定するほうが良いです。JMHプラグインの設定でそれらを指定してしまうとテスト毎の設定が効かず意図してなかった長い計測になる場合があります。

JMHの計測手順とそのコスト構造

1回の計測(measurement)は「実行時間(time&timeUnit)」と「実行回数(iterations)」で決まります。

ただし1回の計測の前にはウォームアップ(Warmup)が実施されます。ウォームアップも計測と同様に実行時間&実行回数が指定されます。

テスト項目ごとにウォームアップと計測を行います。

そしてそれを繰り返すこと(Fork)ができます。

つまり./gradlew jmhを実行した際にかかる時間は以下の式で見積もれます。

{fork回数} x {テスト項目数} x
  ( {ウォームアップ回数} x {ウォームアップ実施時間} + {計測回数} x {計測実施時間} )

JMHプラグインのデフォルト値だとこの計算結果が結構大きくなるので、明示的な設定はほぼ必須と言っても過言ではなくなっています。

JMH用のアノテーションのJavadoc

何より重要な情報です。文章が少なくてサンプルもないのでどう使ったらよいか読んだだけでは分かりにくいのですけれども。

https://javadoc.io/static/org.openjdk.jmh/jmh-core/1.25/org/openjdk/jmh/annotations/package-summary.html

テスト毎に指定したほうが良い項目

とりあえず指定しておいたほうが良いものを3つだけ挙げておきます。

  • Benchmark ベンチマークで実行するメソッドをマークする。
  • BenchmarkMode 何を計測するか。設定可能な値はMode
    デフォルトはスループット(Mode.Throughput)。1回あたりの実行時間が短い=1秒間に複数回実行できるようなテストはコレと何秒間計測するかを指定するのが良い。
    逆に時間がかかるようなテストはシングルショット(Mode.SingleShotTime)の指定をしたうえで何回計測するかを指定するのが良い。
  • Measurement 何秒間および何回計測するかを指定する。
    スループットのときはtime, timeUnitを指定する。
    シングルショットの場合はだいたいのケースでiterationsを指定する。

JMHプラグインですべき設定

だいたいのケースで以下だけ設定しておくのが良さそうです。

jmh {
    fork = 1
    warmup = '1s'
    wormupIterations = 1
}

fork回数

デフォルトでは5回くらいだったと記憶してますが、それによって計測全体の所要時間が膨大になるわりに、1回だけの実行で得られる結果とそう大きな隔たりはなかったので1回で良いと判断しました。

しかしテスト内容や求められる結果の厳密さによっては見直したほうが良い項目です。

ウォームアップ

JVMの性質を考えるとウォームアップは欠かせません。かつてはちゃんとした計測にはそれなりの長さのウォームアップが必要とされていたのですが、現在ではとりあえずの簡易計測なら1回実行しておけばそう外れた結果にはならないようです。なおまったく実施しないのは論外です。

しかしテスト内容や求められる結果の厳密さによっては見直したほうが良い項目です。

目を通すべき公式資料

JMHには使い方を平易に解説する独立した公式文章が見あたりませんでした。その代わりにSampleを読めというスタンスのようです。確かにサンプルコード内にはかなり丁寧にコメントが書かれているので目を通すと良さそうです。私はつまみ食いしかしていませんが。

あとは前出のアノテーションについてのJavadocでしょうか。

まとめ

  • 速いテストケースはスループット、遅いものはシングルショットで計測する
    • スループットは実行時間を指定する(回数を指定しても良い)
    • シングルショットは回数を指定する(時間を指定したらどうなるやろね?)
  • 厳密さを求めないのであればウォームアップは1回1秒程度で良さそう
    • 一度も実施しないのはNG。だいたい計測が下振れする
  • fork? 1回で十分ですよ。わかってくださいよ

参考資料

この記事に贈られたバッジ