1. ホーム
  2. java

[解決済み] Javaにおける例外処理によるパフォーマンスへの影響とは?

2022-03-18 02:12:06

質問

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

従来の常識や多くのGoogleの検索結果では、Javaでは通常のプログラムの流れに例外的なロジックを使うべきではないと言われています。 通常、2つの理由が挙げられます。

  1. 通常のコードよりも1桁以上遅くなることもあります(理由は様々です)。

そして

  1. というのも、人々は例外的なコードで処理されるのはエラーだけだと考えているからです。

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

例として このページ は、Javaの例外処理を「非常に遅い」と説明し、その遅さを例外メッセージ文字列の生成に関連付けます(「この文字列は、次にスローされる例外オブジェクトの生成に使用されます。これは高速ではありません。 Javaにおける効果的な例外処理 その理由は、例外処理のオブジェクト生成の側面にあり、それによって例外のスローイングが本質的に遅くなるためです。 もうひとつの理由は、スタックトレースの生成に時間がかかるからだそうです。

私のテスト(Java 1.6.0_07, Java HotSpot 10.0, 32 bit Linux使用)では、例外処理は通常のコードより遅くはないことがわかりました。 あるコードを実行するループ内のメソッドを実行してみました。 メソッドの最後で、私はブール値を使用して、次のようにするかどうかを示します。 戻る または 投げる . こうすれば、実際の処理は同じになります。私は、JVMがウォームアップしているのかもしれないと思い、異なる順序でメソッドを実行し、テスト時間を平均してみました。 私のすべてのテストでは、スローはリターンより速くなければ、少なくとも同じくらい速かったです(最大3.1%速い)。 私は、私のテストが間違っていた可能性を完全に受け入れますが、コードサンプル、テスト比較、またはここ1、2年の結果で、Javaの例外処理が実際に遅いことを示すものを見たことがありません。

私をこの道に導いたのは、通常の制御ロジックの一部として例外を投げるAPIを使う必要があったことです。 私はその使い方を正したかったのですが、今は正せないかもしれません。 その代わりに、彼らの前向きな考えを褒めるしかないのでしょうか?

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

解決方法は?

例外をどのように実装するかによります。最も簡単な方法は、setjmp と longjmp を使うことです。つまり、CPUのすべてのレジスタがスタックに書き込まれ(これにはすでに時間がかかります)、場合によっては他のデータも作成する必要があります...これらはすべてtry文の中ですでに起こっています。throw文はスタックを解放し、すべてのレジスタの値を復元する必要があります(VM内の他の値も復元される可能性があります)。tryとthrowは等しく遅く、それはかなり遅いです。しかし、例外が投げられない場合、tryブロックの終了にはほとんどの場合全く時間がかかりません(メソッドが存在すれば自動的にクリーンアップされるスタックにすべてが置かれるからです)。

Sunや他の企業も、これが最適でない可能性があることを認識していましたし、もちろんVMは時代とともにどんどん速くなっています。例外を実装する別の方法があり、try自体は非常に高速になり(実は一般的にtryでは何も起こりません - 必要なことはすべてクラスがVMにロードされたときにすでに行われています)、throwもそれほど遅くなりません。どのJVMがこの新しい、より良いテクニックを使っているかは知りませんが...。

...しかし、あなたは、後であなたのコードが1つの特定のシステム上の1つのJVMでのみ実行されるようにJavaで書いているのですか?もしそれが他のプラットフォームや他のJVMバージョン(おそらく他のベンダーの)で実行されるかもしれないので、誰が彼らも高速実装を使用すると言うのですか?高速なものは、低速なものよりも複雑で、すべてのシステムで簡単に実現できるわけではありません。あなたは、ポータブルであり続けたいですか?それなら、例外が高速であることを当てにしないでください。

また、トライブロックの中で何をするかが大きな違いです。トライブロックを開き、そのトライブロック内からは一切メソッドを呼び出さないようにすれば、トライブロックは超高速になり、JITは実際にスローを単純なgotoのように扱えるようになります。例外が発生しても、スタック状態を保存する必要はなく、スタックを解放する必要もありません(キャッチハンドラにジャンプするだけでよいのです)。しかし、これは通常行うことではありません。普通は、トライブロックを開いてから、例外を投げる可能性のあるメソッドを呼び出しますよね?また、メソッド内で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を使うより良い方法があるのは間違いありません。