1. 概要

Javaでは、例外は一般に高価であると見なされており、フロー制御には使用しないでください。 このチュートリアルは、この認識が正しいことを証明し、パフォーマンスの問題の原因を特定します。

2. 環境のセットアップ

パフォーマンスコストを評価するコードを作成する前に、ベンチマーク環境を設定する必要があります。

2.1. Javaマイクロベンチマークハーネス

例外オーバーヘッドの測定は、単純なループでメソッドを実行し、合計時間を記録するほど簡単ではありません。

その理由は、ジャストインタイムコンパイラが邪魔になってコードを最適化できるためです。 このような最適化により、本番環境で実際に実行するよりもコードのパフォーマンスが向上する可能性があります。 言い換えれば、それは偽陽性の結果をもたらすかもしれません。

JVMの最適化を軽減できる制御された環境を作成するために、 Java Microbenchmark Harness 、または略してJMHを使用します。

次のサブセクションでは、JMHの詳細に立ち入ることなく、ベンチマーク環境のセットアップについて説明します。 このツールの詳細については、Javaによるマイクロベンチマークチュートリアルをご覧ください。

2.2. JMHアーティファクトの取得

JMHアーティファクトを取得するには、次の2つの依存関係をPOMに追加します。

<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-core</artifactId>
    <version>1.33</version>
</dependency>
<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-generator-annprocess</artifactId>
    <version>1.33</version>
</dependency>

JMHCoreおよびJMHAnnotation Processor の最新バージョンについては、MavenCentralを参照してください。

2.3. ベンチマーククラス

ベンチマークを保持するためのクラスが必要です。

@Fork(1)
@Warmup(iterations = 2)
@Measurement(iterations = 10)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class ExceptionBenchmark {
    private static final int LIMIT = 10_000;
    // benchmarks go here
}

上記のJMHアノテーションを見てみましょう。

  • @Fork :ベンチマークを実行するためにJMHが新しいプロセスを生成する必要がある回数を指定します。 その値を1に設定して、1つのプロセスのみを生成し、結果が表示されるまで長時間待つことを回避します。
  • @Warmup :ウォームアップパラメータを実行します。 iterations 要素が2であるということは、結果を計算するときに最初の2つの実行が無視されることを意味します
  • @Measurement :測定パラメーターを実行します。 iterations の値が10の場合、JMHは各メソッドを10回実行することを示します。
  • @BenchmarkMode :これはJHMが実行結果を収集する方法です。 値AverageTimeは、メソッドが操作を完了するために必要な平均時間をJMHがカウントすることを要求します
  • @OutputTimeUnit :出力時間の単位を示します。この場合はミリ秒です。

さらに、クラス本体内に静的フィールド、つまりLIMITがあります。 これは、各メソッド本体の反復回数です。

2.4. ベンチマークの実行

ベンチマークを実行するには、mainメソッドが必要です。

public class MappingFrameworksPerformance {
    public static void main(String[] args) throws Exception {
        org.openjdk.jmh.Main.main(args);
    }
}

プロジェクトをJARファイルにパッケージ化し、コマンドラインで実行できます。 もちろん、ベンチマーク手法を追加していないため、これを行うと空の出力が生成されます。

便宜上、maven-jar-pluginをPOMに追加できます。 このプラグインを使用すると、IDE内でmainメソッドを実行できます。

<groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-jar-plugin</artifactId>
    <version>3.2.0</version>
    <configuration>
        <archive>
            <manifest>
                <mainClass>com.baeldung.performancetests.MappingFrameworksPerformance</mainClass>
            </manifest>
        </archive>
    </configuration>
</plugin>

maven-jar-plugin の最新バージョンは、ここにあります。

3. パフォーマンス測定

パフォーマンスを測定するためのベンチマーク方法をいくつか用意するときが来ました。 これらの各メソッドには、@Benchmarkアノテーションを付ける必要があります。

3.1. 正常に戻るメソッド

通常どおりに戻るメソッドから始めましょう。 つまり、例外をスローしないメソッド:

@Benchmark
public void doNotThrowException(Blackhole blackhole) {
    for (int i = 0; i < LIMIT; i++) {
        blackhole.consume(new Object());
    }
}

blackhole パラメーターは、Blackholeのインスタンスを参照します。 これは、デッドコードの除去を防ぐのに役立つJMHクラスであり、ジャストインタイムコンパイラが実行する可能性のある最適化です。

この場合、ベンチマークは例外をスローしません。 実際、これを参照として使用して、例外をスローするもののパフォーマンスを評価します。

main メソッドを実行すると、次のレポートが表示されます。

Benchmark                               Mode  Cnt  Score   Error  Units
ExceptionBenchmark.doNotThrowException  avgt   10  0.049 ± 0.006  ms/op

この結果には特別なことは何もありません。 ベンチマークの平均実行時間は0.049ミリ秒であり、それ自体はまったく意味がありません。

3.2. 例外の作成とスロー

例外をスローしてキャッチする別のベンチマークは次のとおりです。

@Benchmark
public void throwAndCatchException(Blackhole blackhole) {
    for (int i = 0; i < LIMIT; i++) {
        try {
            throw new Exception();
        } catch (Exception e) {
            blackhole.consume(e);
        }
    }
}

出力を見てみましょう:

Benchmark                                  Mode  Cnt   Score   Error  Units
ExceptionBenchmark.doNotThrowException     avgt   10   0.048 ± 0.003  ms/op
ExceptionBenchmark.throwAndCatchException  avgt   10  17.942 ± 0.846  ms/op

メソッドdoNotThrowExceptionの実行時間のわずかな変更は重要ではありません。 これは、基盤となるOSとJVMの状態の変動にすぎません。 重要なポイントは、例外をスローすると、メソッドの実行が数百倍遅くなることです。

次のいくつかのサブセクションでは、そのような劇的な違いに正確につながるものを見つけます。

3.3. 例外をスローせずに作成する

例外を作成、スロー、およびキャッチする代わりに、次のように作成します。

@Benchmark
public void createExceptionWithoutThrowingIt(Blackhole blackhole) {
    for (int i = 0; i < LIMIT; i++) {
        blackhole.consume(new Exception());
    }
}

それでは、宣言した3つのベンチマークを実行してみましょう。

Benchmark                                            Mode  Cnt   Score   Error  Units
ExceptionBenchmark.createExceptionWithoutThrowingIt  avgt   10  17.601 ± 3.152  ms/op
ExceptionBenchmark.doNotThrowException               avgt   10   0.054 ± 0.014  ms/op
ExceptionBenchmark.throwAndCatchException            avgt   10  17.174 ± 0.474  ms/op

結果は驚くかもしれません。最初のメソッドと3番目のメソッドの実行時間はほぼ同じですが、2番目のメソッドの実行時間は大幅に短くなっています。

この時点で、それは明らかです throwステートメントとcatchステートメント自体はかなり安価です。 一方、例外を作成すると、高いオーバーヘッドが発生します。

3.4. スタックトレースを追加せずに例外をスローする

例外の作成が通常のオブジェクトの作成よりもはるかにコストがかかる理由を理解しましょう。

@Benchmark
@Fork(value = 1, jvmArgs = "-XX:-StackTraceInThrowable")
public void throwExceptionWithoutAddingStackTrace(Blackhole blackhole) {
    for (int i = 0; i < LIMIT; i++) {
        try {
            throw new Exception();
        } catch (Exception e) {
            blackhole.consume(e);
        }
    }
}

この方法とサブセクション3.2の方法の唯一の違いは、jvmArgs要素です。 その値-XX:-StackTraceInThrowable はJVMオプションであり、スタックトレースが例外に追加されないようにします。

ベンチマークをもう一度実行してみましょう。

Benchmark                                                 Mode  Cnt   Score   Error  Units
ExceptionBenchmark.createExceptionWithoutThrowingIt       avgt   10  17.874 ± 3.199  ms/op
ExceptionBenchmark.doNotThrowException                    avgt   10   0.046 ± 0.003  ms/op
ExceptionBenchmark.throwAndCatchException                 avgt   10  16.268 ± 0.239  ms/op
ExceptionBenchmark.throwExceptionWithoutAddingStackTrace  avgt   10   1.174 ± 0.014  ms/op

例外にスタックトレースを入力しないことで、実行時間を100倍以上短縮しました。 どうやら、スタックを歩き、そのフレームを例外に追加すると、私たちが見たように鈍感になります。

3.5. 例外をスローし、そのスタックトレースを巻き戻す

最後に、例外をスローし、それをキャッチするときにスタックトレースを巻き戻すとどうなるか見てみましょう。

@Benchmark
public void throwExceptionAndUnwindStackTrace(Blackhole blackhole) {
    for (int i = 0; i < LIMIT; i++) {
        try {
            throw new Exception();
        } catch (Exception e) {
            blackhole.consume(e.getStackTrace());
        }
    }
}

結果は次のとおりです。

Benchmark                                                 Mode  Cnt    Score   Error  Units
ExceptionBenchmark.createExceptionWithoutThrowingIt       avgt   10   16.605 ± 0.988  ms/op
ExceptionBenchmark.doNotThrowException                    avgt   10    0.047 ± 0.006  ms/op
ExceptionBenchmark.throwAndCatchException                 avgt   10   16.449 ± 0.304  ms/op
ExceptionBenchmark.throwExceptionAndUnwindStackTrace      avgt   10  326.560 ± 4.991  ms/op
ExceptionBenchmark.throwExceptionWithoutAddingStackTrace  avgt   10    1.185 ± 0.015  ms/op

スタックトレースを巻き戻すだけで、実行時間は約20倍になります。 言い換えると、例外からスタックトレースをスローするだけでなく抽出すると、パフォーマンスが大幅に低下します。

4. 結論

このチュートリアルでは、例外のパフォーマンスへの影響を分析しました。 具体的には、パフォーマンスコストは、ほとんどの場合、例外にスタックトレースを追加することであることがわかりました。 このスタックトレースが後で巻き戻されると、オーバーヘッドがはるかに大きくなります。

例外のスローと処理にはコストがかかるため、通常のプログラムフローには使用しないでください。代わりに、その名前が示すように、例外は例外的な場合にのみ使用する必要があります。

完全なソースコードはGitHubにあります。