Javaでの例外のパフォーマンスへの影響
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番目のメソッドの実行時間は大幅に短くなっています。
この時点で、それは明らかです
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にあります。