Jmh で Scala のコードのマイクロベンチマークをとる
はじめに
ソフトウェアを改善するためには客観的なデータを集めてベンチワークをとるべきです.
さて、Scala のコレクションを要素ごとにマージする API には Array.zip
と (arr1,arr2).zipped
があります. どちらが速いでしょうか. そして、なぜ速いのでしょうか. 結論を先に言うと zipped のほうが速いです. zipped は処理中の中間配列の割り当て、タプルのアロケーションをする必要がないのでその分パフォーマンスが良いです.
(※ Scala のタプルはオブジェクトの割り当てがあるのでパフォーマンスはあまりよくありません. また配列のサイズが事前にわかっているなら先に割り当てたほうが速いです.)
arr0.zip(arr1).map(/*do something */)
(arr0,arr1).zipped.map(/*do something */) // this is faster
さて、この質問について Scala 界隈の有名 OSS 作者・コントリビューターの Travis Brown さんが stackoverflow でいい回答 をしているので、この記事ではそれを参考にしながら jmh を使ってベンチマークをとる手順をおさらいしましょう. jmh は Java Microbenchmark Harness の略です.
Scala(or JVM) のベンチマークツールにはいろいろあります. Scala でもチューニングには結局 JVM の情報が必要になるので Java 寄りのライブラリを使うケースが一般的なようです. 古い OSS では google の caliper を使っている例もありますが, 私が見る限り多くの OSS では sbt-jmh を介して jmh を使っています. これは jmh の薄いラッパーです.
jmh ではベンチマーク対象のコードにアノテーションを付与して様々な挙動を定義します.
ベンチマークを書く
さて、御託を並べてもしょうがないのでさっそくコードを書きましょう. jmh のバインディングは sbt プラグインとして公開されているのでそれを使ってシュッと設定をしましょう.
addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.3")
プロジェクト全体でベンチマークを有効にするか、ベンチマーク用のサブプロジェクトを作って、そのプロジェクトでベンチマーク用のコードを書きます.
enablePlugins(JmhPlugin)
// または
// lazy val example = project
// .in(projectDir)
// .enablePlugins(JmhPlugin)
今回は arr0.zip(arr1).map(...)
と (arr0,arr1).zipped.map(...)
のどちらが速いのかをはかるので次のようなコードを書きます. また「宣言的」で「可読性の高い」といわれる関数型ライクな書き方のオーバーヘッドがどれくらいあるのかを確かめるいい機会なので for
,while
などの実装についても見てみましょう.
package bench
import org.openjdk.jmh.annotations._
import java.util.concurrent.TimeUnit
@State(Scope.Benchmark)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@BenchmarkMode(Array(Mode.Throughput))
class ZippedBench {
var arr0: Array[Double] = null
var arr1: Array[Double] = null
@Param(Array("100")) // "1000", "1000000"))
var length:Int = 0
@Setup(Level.Iteration)
def setup() :Unit = {
arr0 = Array.fill(length)(math.random)
arr1 = Array.fill(length)(math.random)
}
def benchZip(arr0: Array[Double], arr1: Array[Double]): Array[Double] =
arr0.zip(arr1).map(x => x._1 + x._2)
def benchZipped(arr0: Array[Double], arr1: Array[Double]): Array[Double] =
(arr0, arr1).zipped.map((x, y) => x + y).toArray
def benchFor(arr0: Array[Double], arr1: Array[Double]): Array[Double] = {
val minSize = math.min(arr0.length, arr1.length)
val newArr = new Array[Double](minSize)
for (i <- 0 until minSize) {
newArr(i) = arr0(i) + arr1(i)
}
newArr
}
def benchWhile(arr0: Array[Double], arr1: Array[Double]): Array[Double] = {
val minSize = math.min(arr0.length, arr1.length)
val newArr = new Array[Double](minSize)
var i = 0
while (i < minSize) {
newArr(i) = arr0(i) + arr1(i)
i += 1
}
newArr
}
def benchTabulate(arr0: Array[Double], arr1: Array[Double]): Array[Double] = {
val minSize = math.min(arr0.length, arr1.length)
Array.tabulate(minSize)(i => arr0(i) + arr1(i))
}
@Benchmark
def withZip: Array[Double] = benchZip(arr0, arr1)
@Benchmark
def withZipped: Array[Double] = benchZipped(arr0, arr1)
@Benchmark
def withFor: Array[Double] = benchFor(arr0,arr1)
@Benchmark
def withWhile: Array[Double] = benchWhile(arr0, arr1)
@Benchmark
def withTabulate: Array[Double] = benchTabulate(arr0,arr1)
}
jmh のベンチマークについて
ベンチマークを実行する前に jmh についてもう少し説明します.
var arr0: Array[Double] = null
var arr1: Array[Double] = null
//...
@Setup
def setup() :Unit = {
arr0 = Array.fill(length)(math.random)
arr1 = Array.fill(length)(math.random)
}
@Benchmark
アノテーションをつけたメソッドには ExecutionPlan
, State
や Blackhole
など決められたものしか引数に渡せないのでこのように setup 処理を定義して 異なる長さの arr0, arr1 をベンチマークするメソッドに渡せるようにしています.
@Fork(value = 1, warmups = 2)
まず、 JVM はしばしばループを最適化してしまうので開発者がベンチマーク内で直接ループを書くことは推奨されていません. jmh ではアノテーションを介して jmh にループの回数などを指示します.
上の例では jit 最適化のために warmup する回数を 2回に指定しています. warmup のときのデータは捨てられてベンチマーク結果には含まれません. 長時間、何度も呼び出される処理をベンチマークを計測する際は warmup をするべきですが、短時間、たまにしか呼び出されない処理を計測する場合は少し後で紹介するBenckmarkMode
を SingleShot
にして計測するほうがよさそうです.
``@scala
@OutputTimeUnit(TimeUnit.MILLISECONDS)
結果に表示される時間の単位は`@OutputTimeUnit` で指定します.
```scala
@BenchmarkMode(Array(Mode.Throughput))
また、ベンチマークには複数のモードがあります. 計測したい対象に応じてこれらを指定しましょう.
mode | 内容 |
---|---|
Throughput | 一定時間当たり何回処理を実行できるか. 大きいほど速い. |
AverageTime | 処理の時間に平均どれくらい時間がかかるか. 小さいほど速い. |
SampleTime | 処理の時間にどれくらい時間がかかるか. min, max などをサンプリングする. |
SingleShot | コールドスタートで処理を実行するのにどれくらい時間がかかるか. |
また、セットアップにも複数のモードがあります.
|mode|内容|
|Level.Trial|ウォームアップを含む一連のベンチマークにつき実行されます.|
|Level.Iteration|イテレーションごとにセットアップを実行します.|
|Level.Invocation|ベンチマークメソッドの呼び出しごとにセットアップを実行します.|
アノテーションを付与することで @Param
,@Setup
,@Scope
などのベンチマークの複数の軸のマトリックスを設定できます.
他にも jmh にはベンチマーク結果がおかしくならないようにJVM のウォームアップや最適化を防ぐためにさまざまなヘルパーが用意されています.
例えばblackhole
を使うことで JVM の dead code elimination を回避することができます.
@Benchmark
def benchX(blackhole: Blackhole) = {
// ...
blackhole.consume(localVar);
}
結果
大体説明できたと思うのでベンチマークを実行して結果を見ましょう.
sbt
> example/jmh:run -prof gc bench.ZippedBench
※ イテレーションの回数、warmup の回数などのパラメタは sbt タスクの引数 -i
,-wi
でも指定できます.
まずは Array の長さが 100 のときのケースです.
[info] Benchmark (length) Mode Cnt Score Error Units
[info] ZippedBench.withFor 100 thrpt 25 10545.389 ± 713.504 ops/ms
[info] ZippedBench.withFor:·gc.alloc.rate 100 thrpt 25 7814.800 ± 528.779 MB/sec
[info] ZippedBench.withFor:·gc.alloc.rate.norm 100 thrpt 25 816.038 ± 0.001 B/op
[info] ZippedBench.withFor:·gc.churn.G1_Eden_Space 100 thrpt 25 7839.669 ± 530.038 MB/sec
[info] ZippedBench.withFor:·gc.churn.G1_Eden_Space.norm 100 thrpt 25 818.640 ± 1.671 B/op
[info] ZippedBench.withFor:·gc.churn.G1_Survivor_Space 100 thrpt 25 0.004 ± 0.001 MB/sec
[info] ZippedBench.withFor:·gc.churn.G1_Survivor_Space.norm 100 thrpt 25 ≈ 10⁻³ B/op
[info] ZippedBench.withFor:·gc.count 100 thrpt 25 4357.000 counts
[info] ZippedBench.withFor:·gc.time 100 thrpt 25 10390.000 ms
[info] ZippedBench.withTabulate 100 thrpt 25 10028.418 ± 494.065 ops/ms
[info] ZippedBench.withTabulate:·gc.alloc.rate 100 thrpt 25 7431.567 ± 366.200 MB/sec
[info] ZippedBench.withTabulate:·gc.alloc.rate.norm 100 thrpt 25 816.037 ± 0.001 B/op
[info] ZippedBench.withTabulate:·gc.churn.G1_Eden_Space 100 thrpt 25 7456.992 ± 361.188 MB/sec
[info] ZippedBench.withTabulate:·gc.churn.G1_Eden_Space.norm 100 thrpt 25 818.898 ± 1.781 B/op
[info] ZippedBench.withTabulate:·gc.churn.G1_Survivor_Space 100 thrpt 25 0.004 ± 0.001 MB/sec
[info] ZippedBench.withTabulate:·gc.churn.G1_Survivor_Space.norm 100 thrpt 25 ≈ 10⁻³ B/op
[info] ZippedBench.withTabulate:·gc.count 100 thrpt 25 4080.000 counts
[info] ZippedBench.withTabulate:·gc.time 100 thrpt 25 10232.000 ms
[info] ZippedBench.withWhile 100 thrpt 25 10166.134 ± 654.899 ops/ms
[info] ZippedBench.withWhile:·gc.alloc.rate 100 thrpt 25 7533.468 ± 485.278 MB/sec
[info] ZippedBench.withWhile:·gc.alloc.rate.norm 100 thrpt 25 816.036 ± 0.001 B/op
[info] ZippedBench.withWhile:·gc.churn.G1_Eden_Space 100 thrpt 25 7555.945 ± 489.815 MB/sec
[info] ZippedBench.withWhile:·gc.churn.G1_Eden_Space.norm 100 thrpt 25 818.414 ± 1.797 B/op
[info] ZippedBench.withWhile:·gc.churn.G1_Survivor_Space 100 thrpt 25 0.004 ± 0.001 MB/sec
[info] ZippedBench.withWhile:·gc.churn.G1_Survivor_Space.norm 100 thrpt 25 ≈ 10⁻³ B/op
[info] ZippedBench.withWhile:·gc.count 100 thrpt 25 4008.000 counts
[info] ZippedBench.withWhile:·gc.time 100 thrpt 25 10043.000 ms
[info] ZippedBench.withZip 100 thrpt 25 621.984 ± 64.578 ops/ms
[info] ZippedBench.withZip:·gc.alloc.rate 100 thrpt 25 4771.580 ± 492.907 MB/sec
[info] ZippedBench.withZip:·gc.alloc.rate.norm 100 thrpt 25 8448.486 ± 7.740 B/op
[info] ZippedBench.withZip:·gc.churn.G1_Eden_Space 100 thrpt 25 4779.091 ± 494.497 MB/sec
[info] ZippedBench.withZip:·gc.churn.G1_Eden_Space.norm 100 thrpt 25 8461.487 ± 21.996 B/op
[info] ZippedBench.withZip:·gc.churn.G1_Survivor_Space 100 thrpt 25 0.026 ± 0.006 MB/sec
[info] ZippedBench.withZip:·gc.churn.G1_Survivor_Space.norm 100 thrpt 25 0.046 ± 0.010 B/op
[info] ZippedBench.withZip:·gc.count 100 thrpt 25 3268.000 counts
[info] ZippedBench.withZip:·gc.time 100 thrpt 25 7998.000 ms
[info] ZippedBench.withZipped 100 thrpt 25 999.613 ± 84.087 ops/ms
[info] ZippedBench.withZipped:·gc.alloc.rate 100 thrpt 25 3428.084 ± 288.350 MB/sec
[info] ZippedBench.withZipped:·gc.alloc.rate.norm 100 thrpt 25 3776.233 ± 0.003 B/op
[info] ZippedBench.withZipped:·gc.churn.G1_Eden_Space 100 thrpt 25 3435.676 ± 290.985 MB/sec
[info] ZippedBench.withZipped:·gc.churn.G1_Eden_Space.norm 100 thrpt 25 3784.129 ± 12.943 B/op
[info] ZippedBench.withZipped:·gc.churn.G1_Survivor_Space 100 thrpt 25 0.010 ± 0.001 MB/sec
[info] ZippedBench.withZipped:·gc.churn.G1_Survivor_Space.norm 100 thrpt 25 0.011 ± 0.001 B/op
[info] ZippedBench.withZipped:·gc.count 100 thrpt 25 2519.000 counts
[info] ZippedBench.withZipped:·gc.time 100 thrpt 25 6699.000 ms
さて zip とzipped の gc.churn.PS_Eden_Space.norm を比較すると zip の場合は短命のオブジェクトが for ,while や zipped の時より生成されていることがわかります.
// ..
[info] ZippedBench.withFor:·gc.alloc.rate.norm 100 thrpt 25 816.038 ± 0.001 B/op
// ..
[info] ZippedBench.withWhile:·gc.alloc.rate.norm 100 thrpt 25 816.036 ± 0.001 B/op
// ...
[info] ZippedBench.withZip:·gc.alloc.rate.norm 100 thrpt 25 8448.486 ± 7.740 B/op
[info] ZippedBench.withZip:·gc.churn.G1_Eden_Space 100 thrpt 25 4779.091 ± 494.497 MB/sec
// ...
[info] ZippedBench.withZipped:·gc.alloc.rate.norm 100 thrpt 25 3776.233 ± 0.003 B/op
[info] ZippedBench.withZipped:·gc.churn.G1_Eden_Space 100 thrpt 25 3435.676 ± 290.985 MB/sec
benchWhile がとても速いことにも注目してみましょう. これは while が関数オブジェクトとコレクション操作ではなくただのイテレーションを素直に実行するのでオーバーヘッドが少ないからです. また事前に配列を割り当てているからです. 文字通り桁違いに速いので zip と zipped の差はあまり気にならなくなってしまいますね(´・ω・`) パフォーマンスが重要な場面では while
を使うべきです. ちなみに Scala の数値計算系ライブラリの spire にはパフォーマンスのために for
を while
にコンパイル時に書き換えるマクロが用意されています.
import spire.syntax.fastFor._
// print numbers 1 through 10
fastFor(0)(_ < 10, _ + 1) { i =>
println(i)
}
benchTabulate もシンタックスの簡潔さと比較して悪くない数字を出しています. これも Arrayの大きさが事前にわかっている場合は前もって割り当てしておけば Array のサイズを追加するオーバーヘッドを回避できるからです.
Scala のコレクションについて詳しくパフォーマンスをとった結果を紹介している lihaoyi さんのブログ記事からも、割り当て済み配列(Array-prealloc) のパフォーマンスがとてもいいことが見て取れます. この記事は Scala の公式ドキュメントのコレクションのパフォーマンスが説明しきれていない部分をしっかり説明してくれているので Scala でパフォーマンスを気にするなら一度読んでみるといいでしょう.
次は配列の大きさを 10000 にして再度ベンチマークをとってみましょう.
要素数のオーダーによって同じプログラムでもパフォーマンスが変化することがあります. この変化がデータ量に応じてどのように変化するか注意する必要があります. データ構造やアルゴリズムにはデータ数が多い場合はパフォーマンスが良い一方でデータが少ない場合はあまりパフォーマンスがよくないケース、あるいはその逆のケースがあります. これは↑の lihaoyi さんのブログ記事のデータを見てみてみるとわかりやすいです.
実際のアプリケーションではどれくらいの大きさのデータを扱うのか見積もれるとよりよいパフォーマンスチューニングができるはずです.
[info] Benchmark (length) Mode Cnt Score Error Units
[info] ZippedBench.withFor 10000 thrpt 25 107.929 ± 5.136 ops/ms
[info] ZippedBench.withFor:·gc.alloc.rate 10000 thrpt 25 7842.329 ± 373.072 MB/sec
[info] ZippedBench.withFor:·gc.alloc.rate.norm 10000 thrpt 25 80020.112 ± 0.109 B/op
[info] ZippedBench.withFor:·gc.churn.G1_Eden_Space 10000 thrpt 25 7896.289 ± 376.114 MB/sec
[info] ZippedBench.withFor:·gc.churn.G1_Eden_Space.norm 10000 thrpt 25 80570.446 ± 167.376 B/op
[info] ZippedBench.withFor:·gc.churn.G1_Survivor_Space 10000 thrpt 25 0.019 ± 0.001 MB/sec
[info] ZippedBench.withFor:·gc.churn.G1_Survivor_Space.norm 10000 thrpt 25 0.192 ± 0.010 B/op
[info] ZippedBench.withFor:·gc.count 10000 thrpt 25 4230.000 counts
[info] ZippedBench.withFor:·gc.time 10000 thrpt 25 10299.000 ms
[info] ZippedBench.withTabulate 10000 thrpt 25 107.724 ± 9.385 ops/ms
[info] ZippedBench.withTabulate:·gc.alloc.rate 10000 thrpt 25 7827.217 ± 681.723 MB/sec
[info] ZippedBench.withTabulate:·gc.alloc.rate.norm 10000 thrpt 25 80020.331 ± 0.165 B/op
[info] ZippedBench.withTabulate:·gc.churn.G1_Eden_Space 10000 thrpt 25 7878.829 ± 687.149 MB/sec
[info] ZippedBench.withTabulate:·gc.churn.G1_Eden_Space.norm 10000 thrpt 25 80547.058 ± 125.516 B/op
[info] ZippedBench.withTabulate:·gc.churn.G1_Survivor_Space 10000 thrpt 25 0.019 ± 0.001 MB/sec
[info] ZippedBench.withTabulate:·gc.churn.G1_Survivor_Space.norm 10000 thrpt 25 0.195 ± 0.017 B/op
[info] ZippedBench.withTabulate:·gc.count 10000 thrpt 25 4465.000 counts
[info] ZippedBench.withTabulate:·gc.time 10000 thrpt 25 10845.000 ms
[info] ZippedBench.withWhile 10000 thrpt 25 106.784 ± 12.534 ops/ms
[info] ZippedBench.withWhile:·gc.alloc.rate 10000 thrpt 25 7759.108 ± 910.523 MB/sec
[info] ZippedBench.withWhile:·gc.alloc.rate.norm 10000 thrpt 25 80020.140 ± 0.200 B/op
[info] ZippedBench.withWhile:·gc.churn.G1_Eden_Space 10000 thrpt 25 7814.662 ± 919.085 MB/sec
[info] ZippedBench.withWhile:·gc.churn.G1_Eden_Space.norm 10000 thrpt 25 80589.199 ± 152.628 B/op
[info] ZippedBench.withWhile:·gc.churn.G1_Survivor_Space 10000 thrpt 25 0.019 ± 0.001 MB/sec
[info] ZippedBench.withWhile:·gc.churn.G1_Survivor_Space.norm 10000 thrpt 25 0.197 ± 0.025 B/op
[info] ZippedBench.withWhile:·gc.count 10000 thrpt 25 4199.000 counts
[info] ZippedBench.withWhile:·gc.time 10000 thrpt 25 10431.000 ms
[info] ZippedBench.withZip 10000 thrpt 25 5.421 ± 0.469 ops/ms
[info] ZippedBench.withZip:·gc.alloc.rate 10000 thrpt 25 4135.340 ± 357.686 MB/sec
[info] ZippedBench.withZip:·gc.alloc.rate.norm 10000 thrpt 25 840175.666 ± 3.198 B/op
[info] ZippedBench.withZip:·gc.churn.G1_Eden_Space 10000 thrpt 25 4144.427 ± 357.885 MB/sec
[info] ZippedBench.withZip:·gc.churn.G1_Eden_Space.norm 10000 thrpt 25 842060.986 ± 2273.471 B/op
[info] ZippedBench.withZip:·gc.churn.G1_Survivor_Space 10000 thrpt 25 1.729 ± 0.626 MB/sec
[info] ZippedBench.withZip:·gc.churn.G1_Survivor_Space.norm 10000 thrpt 25 361.943 ± 129.956 B/op
[info] ZippedBench.withZip:·gc.count 10000 thrpt 25 3176.000 counts
[info] ZippedBench.withZip:·gc.time 10000 thrpt 25 7682.000 ms
[info] ZippedBench.withZipped 10000 thrpt 25 13.356 ± 0.793 ops/ms
[info] ZippedBench.withZipped:·gc.alloc.rate 10000 thrpt 25 4367.561 ± 259.319 MB/sec
[info] ZippedBench.withZipped:·gc.alloc.rate.norm 10000 thrpt 25 360163.802 ± 0.938 B/op
[info] ZippedBench.withZipped:·gc.churn.G1_Eden_Space 10000 thrpt 25 4374.518 ± 259.326 MB/sec
[info] ZippedBench.withZipped:·gc.churn.G1_Eden_Space.norm 10000 thrpt 25 360744.065 ± 884.520 B/op
[info] ZippedBench.withZipped:·gc.churn.G1_Survivor_Space 10000 thrpt 25 0.524 ± 0.198 MB/sec
[info] ZippedBench.withZipped:·gc.churn.G1_Survivor_Space.norm 10000 thrpt 25 44.326 ± 17.612 B/op
[info] ZippedBench.withZipped:·gc.count 10000 thrpt 25 3427.000 counts
[info] ZippedBench.withZipped:·gc.time 10000 thrpt 25 7921.000 ms
[info] ZippedBench.withZip 10000 thrpt 25 5.421 ± 0.469 ops/ms
// ..
[info] ZippedBench.withZip:·gc.alloc.rate.norm 10000 thrpt 25 840175.666 ± 3.198 B/op
// ..
[info] ZippedBench.withZipped 10000 thrpt 25 13.356 ± 0.793 ops/ms
// ..
[info] ZippedBench.withZipped:·gc.alloc.rate.norm 10000 thrpt 25 360163.802 ± 0.938 B/op
// ..
GC に関係するパラメータは長さに比例して変化していますが、スループットはデータ量が増えると zip のほうがだいぶ悪くなっていますね🤔 データ量が多いときは zip と比べたら zipped のほうがマシですね. とはいえ 相変わらず for, while, tabulate のほうが桁違いに速いです. パフォーマンスは大体想定通りですが、for, tabulate, while の順に誤差が大きくなっているのも気になりますね🤔
さて、ベンチマークはあくまではかろうとしたものしかはかれないことに注意しなければなりません. 何を計測するか,その結果をどう解釈すべきかは計測者にゆだねられています.
REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
[info] why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
[info] experiments, perform baseline and negative tests that provide experimental control, make sure
[info] the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
[info] Do not assume the numbers tell you what you want them to tell.
sbt-jmh のレポジトリの About には Trust no one, bench everything.
とありますが、benchmark の結果の数字もちゃんと眉に唾を付けて判断しましょう.
Further Reading
jmh の正しい使い方については jmh の github にあるサンプルが最も網羅的で参考になるはずなのでベンチマークを書く前に一通り目を通しておくといいでしょう.
また、途中で少し説明した JVM の最適化によるベンチマークの落とし穴については以下の記事が最適化アリ/ナシの結果の数字も載せているので参考になります.
実際にプロダクションで使われている Scala ライブラリのベンチマークについては、 cats-effect3 と zio のベンチマークの記事がおすすめです.
jmh に plot 機能はないのでサードパーティの plot ツールを使いましょう.
余談
Java のアノテーションは言語機能の貧しさをその場しのぎで埋め合わせるための,お世辞にもセンスがいい使い方とは言えないものがしばしばあって頭が痛くなります.(golang のように言語機能の貧しさをごまかさず素直に書くスタイルのほうが健全だと思います. あるいは Scala・Kotlin など、必要な機能がもともと用意されているものを使うべきだと思います.)
(というかコンパイル時にわかる情報をランタイムリフレクションで使うな(#^ω^). IDE に依存するコード生成をすな(#^ω^). ちゃんと保守して更新して Java のバージョンを上げろ(#^ω^))
しかし、その点についてはjmh は比較的筋のいいアノテーション(とランタイムマクロ)の使い方だと私は思います. というのは、ベンチマークは書かれたコードの扱うドメインとは異なる軸でソースコードを評価するためのものである、と考えるからです. ベンチマークはランタイムの情報が得られること、ベンチマーク対象のソースコードをなるべく改変しないこと(仮にベンチマークのために特殊なコードをいくつも余分に書かなければならないならそのベンチマークから得られるデータは現実的なユースケースに合致するだろうか)、柔軟にパラメータを変更できること、必要に応じてつけたり外したりできることが期待されます. jmh のアノテーションはこれらのユースケースに合致していると思います.
Discussion