Javaによるマイクロベンチマーク

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 CoreJMH 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にあります。