Java における例外のパフォーマンスへの影響は何ですか? 質問する

Java における例外のパフォーマンスへの影響は何ですか? 質問する

質問: Java での例外処理は実際には遅いのでしょうか?

常識や多くのGoogle検索結果では、例外ロジックはJavaの通常のプログラムフローには使用すべきではないとされています。通常、2つの理由が挙げられます。

  1. それは本当に遅いです - 通常のコードよりも桁違いに遅いです(理由は様々です)。

そして

  1. 例外コードではエラーのみが処理されると想定されているため、これは面倒です。

この質問は#1についてです。

例えば、このページJava の例外処理は「非常に遅い」と説明されており、その遅さは例外メッセージ文字列の作成に関係している - 「この文字列は、スローされる例外オブジェクトの作成に使用されます。これは高速ではありません。」記事Java での効果的な例外処理「その理由は、例外処理のオブジェクト作成の側面によるもので、これによって例外のスローが本質的に遅くなるためです」と書かれています。もう 1 つの理由は、スタック トレースの生成が速度を低下させるということです。

私のテスト (32 ビット Linux 上の Java 1.6.0_07、Java HotSpot 10.0 を使用) では、例外処理は通常のコードよりも遅くないことがわかりました。何らかのコードを実行するループ内でメソッドを実行してみました。メソッドの最後で、ブール値を使用して return するかthrowするを示します。この方法では実際の処理は同じになります。JVM のウォームアップが原因かもしれないと考え、メソッドを異なる順序で実行し、テスト時間を平均化してみました。すべてのテストで、throw は return と少なくとも同じ速さでした。場合によってはもっと速かったです (最大 3.1% 高速)。私のテストが間違っていた可能性は完全にありますが、コード サンプル、テスト比較、または過去 1 ~ 2 年間の結果で、Java の例外処理が実際に遅いことを示すものは見たことがありません。

私がこの道に進むことになったきっかけは、通常の制御ロジックの一部として例外をスローする API を使用する必要があったことです。使用方法を修正したかったのですが、今ではそれができないかもしれません。代わりに、彼らの先見の明を称賛する必要があるのでしょうか?

論文ではジャストインタイムコンパイルにおける効率的な Java 例外処理著者らは、例外がスローされない場合でも、例外ハンドラが存在するだけで、JIT コンパイラがコードを適切に最適化できなくなり、速度が低下すると示唆しています。この理論はまだテストしていません。

ベストアンサー1

例外がどのように実装されるかによって異なります。最も簡単な方法は、setjmp と longjmp を使用することです。つまり、CPU のすべてのレジスタがスタックに書き込まれ (すでに時間がかかります)、他のデータも作成する必要があります... これらはすべて try ステートメントですでに行われています。throw ステートメントはスタックを巻き戻し、すべてのレジスタの値 (および VM 内のその他の値) を復元する必要があります。したがって、try と throw は同じように遅く、かなり遅いですが、例外がスローされない場合は、ほとんどの場合、try ブロックを終了するのにまったく時間がかかりません (すべてがスタックに配置され、メソッドが存在する場合は自動的にクリーンアップされるため)。

Sun やその他の企業は、これがおそらく最適ではないことを認識しており、もちろん VM は時間の経過とともにどんどん高速化しています。例外を実装する別の方法があり、これにより try 自体が非常に高速になり (実際には try ではまったく何も起こりません。クラスが VM によってロードされたときに、必要なことはすべて既に完了しています)、throw がそれほど遅くならなくなります。どの JVM がこの新しい、より優れた手法を使用しているかはわかりません...

...しかし、Java でコードを記述すると、後で特定のシステム上の 1 つの JVM でのみ実行されますか? 他のプラットフォームまたは他の JVM バージョン (他のベンダーの可能性もあります) で実行される可能性がある場合、高速実装も使用すると誰が言うでしょうか? 高速実装は低速実装よりも複雑で、すべてのシステムで簡単に実現できるわけではありません。移植性を維持したいですか? 例外が高速であることに頼らないでください。

try ブロック内で何を行うかによっても大きな違いが生じます。try ブロックを開いて、この try ブロック内からメソッドを呼び出さない場合、JIT は throw を単純な goto のように扱うことができるため、try ブロックは非常に高速になります。例外がスローされた場合、スタック状態を保存する必要も、スタックをアンワインドする必要もありません (catch ハンドラーにジャンプするだけで済みます)。ただし、これは通常行うことではありません。通常は try ブロックを開いてから、例外をスローする可能性のあるメソッドを呼び出します。また、メソッド内で try ブロックを使用する場合でも、他のメソッドを呼び出さないメソッドとはどのようなメソッドでしょうか。数値を計算するだけでしょうか。では、例外は何のために必要なのでしょうか。プログラム フローを制御するはるかにエレガントな方法があります。単純な計算以外のほとんどの操作では、外部メソッドを呼び出す必要があり、これでローカル try ブロックの利点が失われます。

次のテストコードを参照してください。

public class Test {
    int value;


    public int getValue() {
        return value;
    }

    public void reset() {
        value = 0;
    }

    // Calculates without exception
    public void method1(int i) {
        value = ((value + i) / i) << 1;
        // Will never be true
        if ((i & 0xFFFFFFF) == 1000000000) {
            System.out.println("You'll never see this!");
        }
    }

    // Could in theory throw one, but never will
    public void method2(int i) throws Exception {
        value = ((value + i) / i) << 1;
        // Will never be true
        if ((i & 0xFFFFFFF) == 1000000000) {
            throw new Exception();
        }
    }

    // This one will regularly throw one
    public void method3(int i) throws Exception {
        value = ((value + i) / i) << 1;
        // i & 1 is equally fast to calculate as i & 0xFFFFFFF; it is both
        // an AND operation between two integers. The size of the number plays
        // no role. AND on 32 BIT always ANDs all 32 bits
        if ((i & 0x1) == 1) {
            throw new Exception();
        }
    }

    public static void main(String[] args) {
        int i;
        long l;
        Test t = new Test();

        l = System.currentTimeMillis();
        t.reset();
        for (i = 1; i < 100000000; i++) {
            t.method1(i);
        }
        l = System.currentTimeMillis() - l;
        System.out.println(
            "method1 took " + l + " ms, result was " + t.getValue()
        );

        l = System.currentTimeMillis();
        t.reset();
        for (i = 1; i < 100000000; i++) {
            try {
                t.method2(i);
            } catch (Exception e) {
                System.out.println("You'll never see this!");
            }
        }
        l = System.currentTimeMillis() - l;
        System.out.println(
            "method2 took " + l + " ms, result was " + t.getValue()
        );

        l = System.currentTimeMillis();
        t.reset();
        for (i = 1; i < 100000000; i++) {
            try {
                t.method3(i);
            } catch (Exception e) {
                // Do nothing here, as we will get here
            }
        }
        l = System.currentTimeMillis() - l;
        System.out.println(
            "method3 took " + l + " ms, result was " + t.getValue()
        );
    }
}

結果:

method1 took 972 ms, result was 2
method2 took 1003 ms, result was 2
method3 took 66716 ms, result was 2

try ブロックによる速度低下は、バックグラウンド プロセスなどの交絡要因を排除するには小さすぎます。しかし、catch ブロックによってすべてが強制終了され、速度が 66 倍も遅くなりました。

前述したように、try/catch と throw をすべて同じメソッド (method3) 内に配置すれば、結果はそれほど悪くありませんが、これは特別な JIT 最適化なので、頼りにはなりません。また、この最適化を使用しても、throw は依然としてかなり低速です。したがって、ここで何をしようとしているのかはわかりませんが、try/catch/throw を使用するよりも優れた方法が確実にあります。

おすすめ記事