Javaを使用したマイクロベンチマーク
1. 前書き
このクイック記事は、JMH(Java Microbenchmark Harness)に焦点を当てています。 これは、JDK 12以降のJDKに追加されました。以前のバージョンでは、プロジェクトに依存関係を明示的に追加する必要があります。
簡単に言えば、JMHはJVMのウォームアップやコード最適化パスなどの処理を行い、ベンチマークを可能な限りシンプルにします。
2. 入門
開始するには、実際にJava 8で作業を続け、単純に依存関係を定義できます。
org.openjdk.jmh
jmh-core
1.19
org.openjdk.jmh
jmh-generator-annprocess
1.19
JMH CoreとJMH Annotation Processorの最新バージョンは、MavenCentralにあります。
次に、(任意のパブリッククラスで)@Benchmarkアノテーションを利用して簡単なベンチマークを作成します。
@Benchmark
public void init() {
// Do nothing
}
次に、ベンチマークプロセスを開始するメインクラスを追加します。
public class BenchmarkRunner {
public static void main(String[] args) throws Exception {
org.openjdk.jmh.Main.main(args);
}
}
BenchmarkRunnerを実行すると、おそらくやや役に立たないベンチマークが実行されます。 実行が完了すると、要約表が表示されます。
# Run complete. Total time: 00:06:45
Benchmark Mode Cnt Score Error Units
BenchMark.init thrpt 200 3099210741.962 ± 17510507.589 ops/s
3. ベンチマークの種類
JMHは、いくつかの可能なベンチマークをサポートしています:Throughput,AverageTime,SampleTime、およびSingleShotTime。 これらは、@BenchmarkModeアノテーションを介して構成できます。
@Benchmark
@BenchmarkMode(Mode.AverageTime)
public void init() {
// Do nothing
}
結果のテーブルには、スループットではなく平均時間メトリックがあります。
# Run complete. Total time: 00:00:40
Benchmark Mode Cnt Score Error Units
BenchMark.init avgt 20 ≈ 10⁻⁹ s/op
4. ウォームアップと実行の構成
@Forkアノテーションを使用することで、ベンチマークの実行方法を設定できます。valueパラメーターはベンチマークの実行回数を制御し、warmupパラメーターはベンチマークの実行回数を制御します。結果が収集される前にドライランします。例:
@Benchmark
@Fork(value = 1, warmups = 2)
@BenchmarkMode(Mode.Throughput)
public void init() {
// Do nothing
}
これにより、JMHは2つのウォームアップフォークを実行し、リアルタイムベンチマークに移行する前に結果を破棄します。
また、@Warmupアノテーションを使用して、ウォームアップの反復回数を制御できます。 たとえば、@Warmup(iterations = 5)は、デフォルトの20ではなく、5回のウォームアップ反復で十分であることをJMHに通知します。
5. 状態
ここで、Stateを使用して、ハッシュアルゴリズムのベンチマークを行う、それほど簡単ではなく、よりわかりやすいタスクを実行する方法を調べてみましょう。 パスワードを数百回ハッシュすることにより、パスワードデータベースに対する辞書攻撃からの保護を強化するとします。
Stateオブジェクトを使用して、パフォーマンスへの影響を調べることができます。
@State(Scope.Benchmark)
public class ExecutionPlan {
@Param({ "100", "200", "300", "500", "1000" })
public int iterations;
public Hasher murmur3;
public String password = "4v3rys3kur3p455w0rd";
@Setup(Level.Invocation)
public void setUp() {
murmur3 = Hashing.murmur3_128().newHasher();
}
}
ベンチマーク方法は次のようになります。
@Fork(value = 1, warmups = 1)
@Benchmark
@BenchmarkMode(Mode.Throughput)
public void benchMurmur3_128(ExecutionPlan plan) {
for (int i = plan.iterations; i > 0; i--) {
plan.murmur3.putString(plan.password, Charset.defaultCharset());
}
plan.murmur3.hash();
}
ここで、フィールドiterationsには、ベンチマークメソッドに渡されるときに、JMHによって@Paramアノテーションから適切な値が入力されます。 @Setupアノテーション付きメソッドは、ベンチマークを呼び出すたびに呼び出され、新しいHasherを作成して分離を保証します。
実行が完了すると、次のような結果が得られます。
# Run complete. Total time: 00:06:47
Benchmark (iterations) Mode Cnt Score Error Units
BenchMark.benchMurmur3_128 100 thrpt 20 92463.622 ± 1672.227 ops/s
BenchMark.benchMurmur3_128 200 thrpt 20 39737.532 ± 5294.200 ops/s
BenchMark.benchMurmur3_128 300 thrpt 20 30381.144 ± 614.500 ops/s
BenchMark.benchMurmur3_128 500 thrpt 20 18315.211 ± 222.534 ops/s
BenchMark.benchMurmur3_128 1000 thrpt 20 8960.008 ± 658.524 ops/s
5. 結論
このチュートリアルでは、Javaのマイクロベンチマークハーネスに焦点を当てて紹介しました。
いつものように、コード例はon GitHubにあります。