Javaでカジュアルにマイクロベンチする際のTips
はじめに
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'
}
repositories
はjcenter()
が必要らしいですが、だいたい入ってるでしょうからその例は省略します。
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
何より重要な情報です。文章が少なくてサンプルもないのでどう使ったらよいか読んだだけでは分かりにくいのですけれども。
テスト毎に指定したほうが良い項目
とりあえず指定しておいたほうが良いものを3つだけ挙げておきます。
-
Benchmark
ベンチマークで実行するメソッドをマークする。 -
BenchmarkMode
何を計測するか。設定可能な値はMode
。
デフォルトはスループット(Mode.Throughput
)。1回あたりの実行時間が短い=1秒間に複数回実行できるようなテストはコレと何秒間計測するかを指定するのが良い。
逆に時間がかかるようなテストはシングルショット(Mode.SingleShotTime
)の指定をしたうえで何回計測するかを指定するのが良い。 -
Measurement
何秒間および何回計測するかを指定する。
スループットのときはtime
,timeUnit
を指定する。
シングルショットの場合はだいたいのケースでiterations
を指定する。
JMHプラグインですべき設定
だいたいのケースで以下だけ設定しておくのが良さそうです。
jmh {
fork = 1
warmup = '1s'
warmupIterations = 1
}
fork回数
デフォルトでは5回くらいだったと記憶してますが、それによって計測全体の所要時間が膨大になるわりに、1回だけの実行で得られる結果とそう大きな隔たりはなかったので1回で良いと判断しました。
しかしテスト内容や求められる結果の厳密さによっては見直したほうが良い項目です。
ウォームアップ
JVMの性質を考えるとウォームアップは欠かせません。かつてはちゃんとした計測にはそれなりの長さのウォームアップが必要とされていたのですが、現在ではとりあえずの簡易計測なら1回実行しておけばそう外れた結果にはならないようです。なおまったく実施しないのは論外です。
しかしテスト内容や求められる結果の厳密さによっては見直したほうが良い項目です。
目を通すべき公式資料
JMHには使い方を平易に解説する独立した公式文章が見あたりませんでした。その代わりにSampleを読めというスタンスのようです。確かにサンプルコード内にはかなり丁寧にコメントが書かれているので目を通すと良さそうです。私はつまみ食いしかしていませんが。
あとは前出のアノテーションについてのJavadocでしょうか。
まとめ
- 速いテストケースはスループット、遅いものはシングルショットで計測する
- スループットは実行時間を指定する(回数を指定しても良い)
- シングルショットは回数を指定する(時間を指定したらどうなるやろね?)
- 厳密さを求めないのであればウォームアップは1回1秒程度で良さそう
- 一度も実施しないのはNG。だいたい計測が下振れする
- fork? 1回で十分ですよ。わかってくださいよ
参考資料
- JMH公式? 情報量ゼロ
- JMHソースレポジトリ こっちが実質公式なんだろう。大まかな使い方とか各種ツールへのリンクがある。サンプルは見といたほうが良い。
- JMHのJavadoc いろんなパッケージがあるが、基本用があるのはアノテーションだけだろう。
- JMHのGradleプラグイン 最新でもちょっと古いJMHを使ってるのと、Gradle 7.0でdeprecate予定の機能を使ってる疑惑がある。
- koron/java-duckdb-playground JMHを試してみたプロジェクト。living exampleではあるがいつまでliveだかは保証できない。
Discussion
s/wormupIterations/warmupIterations/
ですね。