0

可能な重複:

Javaの例外はどれくらい遅いですか?

次の2つのプログラムは、実行にほぼ同じ時間がかかります。

public class Break {
    public static void main(String[] argv){
        long r = 0, s = 0, t = 0;
        for(long x = 10000000; x > 0; x--){
            long y = x;
            while(y != 1){
                if(y == 0)
                    throw new AssertionError();
                try2: {
                    try1: {
                        for(;;){
                            r++;
                            if(y%2 == 0)
                                break try1;
                            y = y*3 + 1;
                        }
                    }/*catch(Thr _1)*/{
                        for(;;){
                            s++;
                            if(y%2 == 1)
                                break try2;
                            y = y/2;
                        }
                    }
                }/*catch(Thr _2)*/{
                    t++;
                }
            }
        }
        System.out.println(r + ", " + s + ", " + t);
    }
}
public class Try {
    private static class Thr extends Throwable {}

    private static final Thr thrown = new Thr();

    public static void main(String[] argv){
        long r = 0, s = 0, t = 0;
        for(long x = 10000000; x > 0; x--){
            long y = x;
            while(y != 1){
                try{
                    if(y == 0)
                        throw new AssertionError();
                    try{
                        for(;;){
                            r++;
                            if(y%2 == 0)
                                throw thrown;
                            y = y*3 + 1;
                        }
                    }catch(Thr _1){
                        for(;;){
                            s++;
                            if(y%2 == 1)
                                throw thrown;
                            y = y/2;
                        }
                    }
                }catch(Thr _2){
                    t++;
                }
            }
        }
        System.out.println(r + ", " + s + ", " + t);
    }
}
$ for x in Break Try; do echo $x; time java $x; done
Break
1035892632, 1557724831, 520446316

real    0m10.733s
user    0m10.719s
sys     0m0.016s
Try
1035892632, 1557724831, 520446316

real    0m11.218s
user    0m11.204s
sys     0m0.017s

しかし、次の2つのプログラムにかかる時間は比較的異なります。

public class Return {
    private static int tc = 0;

    public static long find(long value, long target, int depth){
        if(depth > 100)
            return -1;
        if(value%100 == target%100){
            tc++;
            return depth;
        }
        long r = find(target, value*29 + 4221673238171300827l, depth + 1);
        return r != -1? r : find(target, value*27 + 4494772161415826936l, depth + 1);
    }

    public static void main(String[] argv){
        long s = 0;
        for(int x = 0; x < 1000000; x++){
            long r = find(0, x, 0);
            if(r != -1)
                s += r;
        }
        System.out.println(s + ", " + tc);
    }
}
public class Throw {
    public static class Found extends Throwable {
        // note the static!
        public static int value = 0;
    }

    private static final Found found = new Found();

    private static int tc = 0;

    public static void find(long value, long target, int depth) throws Found {
        if(depth > 100)
            return;
        if(value%100 == target%100){
            Found.value = depth;
            tc++;
            throw found;
        }
        find(target, value*29 + 4221673238171300827l, depth + 1);
        find(target, value*27 + 4494772161415826936l, depth + 1);
    }

    public static void main(String[] argv){
        long s = 0;
        for(int x = 0; x < 1000000; x++)
            try{
                find(0, x, 0);
            }catch(Found _){
                s += found.value;
            }
        System.out.println(s + ", " + tc);
    }
}
$ for x in Return Throw; do echo $x; time java $x; done
Return
84227391, 1000000

real    0m2.437s
user    0m2.429s
sys     0m0.017s
Throw
84227391, 1000000

real    0m9.251s
user    0m9.215s
sys     0m0.014s

単純なtry / throw / catchメカニズムは、少なくとも部分的に末尾の呼び出しに最適化された戻り値のように見えます(したがって、制御が(最も近いcatch)に戻る場所は直接わかっています)。もちろん、JRE実装は多くの最適化を行います。

後者に大きな違いがあるが前者には違いがあるのはなぜですか?それは、制御フロー分析が前の2つのプログラムをほぼ同じであると判断し、実際のtry / throw / catchesが特に遅いからであるか、それともReturnのためです。findThrowのメソッド呼び出しはできないが、メソッド呼び出しを回避するようなレベルに展開されます。 ありがとう。

編集:この質問は私には異なるようですJavaの例外はどれくらい遅いですか?なぜこれと似たようなケースでこんなに大きな違いがあるのか尋ねないからです。例外オブジェクトの作成に時間が費やされているという事実も無視されます。fillInStackTraceオーバーライドされます - スタックをトラバースしてそれのために配列を作成するようなものを含みます)。しかし、それは私の質問の一部には明らかに答えます。「制御フロー分析が前の2つのプログラムをほぼ同じと決定するからでしょうか」 - スタックトレースを行う答えで言及されたのは奇妙に思えますこれは、スタックが見られないことを判断するための複雑な分析をしない限り、実際のthrow / catch操作を邪魔することになると思われます(@ Stephenの答えは奇妙になるでしょう)。


  • (i)マイクロベンチマークに注意 - 彼らは作るのが難しい。 (ii)よろしいですかfindReturnとThrowで同じ回数と呼ばれますか? - assylias
  • (i)おかげで、この質問をしたのもその理由の一部です。ある構成要素が他の構成要素よりも速い要素であるとは宣言していません。 (ii)確かに..以外の値を返す-1中にReturnのようなものですThrow - この結果が直接に引き上げられていることはかなりわかりやすいでしょう。mainメソッドの最後の2行(他の唯一の呼び出し)を見てください。 - sigynsurfeit
  • 例外は、例外がない場合よりも常に低速です。 - Stu
  • @Stuどうやら彼らは時々そんなに遅くないわけではない(BreakTry)以下の答えでStephenが指摘するように、JVM開発者がそれを行うことのコストがそれを行うことの利益を上回るかもしれないと決心したので、それはそれらを使うコードを最適化することの欠如によるのかもしれません。 - sigynsurfeit
  • @Stuまた、それぞれに対して例外は作成されません。throw通常、誰かが"例外"を発生させたときに行われます。両方のテストで、私は単に同じものを投げますThrowableオブジェクト、一部は一種のシグナルとして。 - sigynsurfeit

1 답변


2

あなたのベンチマークはJVMウォームアップ効果を考慮に入れていません。そのため、実際のプログラムでtry / break / returnがどのように実行されるのかが、実際に見ている結果から明らかになっていることには、かなりの疑問があります。

(メソッド内の各時間テストを宣言し、メソッドを複数回呼び出す必要があります。その後、最初の数回の呼び出しからの出力を破棄するか、数値が安定するまで... JITの一時的なコストを排除します。数字からのコンパイル、クラスのロードなど)


何が起こっているのかを本当に知りたいのであれば、JITコンパイラが各ケースに対して生成するネイティブコードをダンプするようにする必要があります。

私はあなたが最初のケースでJITコンパイラがthrow / catchを回していることに気付くだろうと思いますメソッド内単純な分岐命令に。 2番目のケースでは、JITコンパイラがより複雑なコードを生成している可能性があります。おそらくこれがブランチと同等であると認識していないためです。

なぜ違いがありますか?まあ、JITオプティマイザによって試みられる各最適化のための費用対効果のトレードオフがあります。 JITコンパイラーによってサポートされるそれぞれの新しい最適化には、実装と保守のコストがあります。そして実行時に、コンパイラは最適化のための前提条件が満たされているかどうか見るためにそれがコンパイルしているコードを調べる必要があります。そうであれば最適化を実行することができます。そうでなければ、JITコンパイラーは時間を浪費しています。

これらの例では、(ある場合には)同じメソッド内でスローされてキャッチされ、(もう1つの場合には)メソッド呼び出しと戻り値の境界を越えて伝播するという例外があります。前者の例では、最適化と(推定される)最適化されたコードシーケンスのための十分な前提条件は、どちらもかなり簡単です。後者の例では、オプティマイザは、例外をスローしてキャッチするメソッドが異なるコンパイル単位にある(したがって、再ロードされる可能性がある)可能性、オーバーライドする可能性などに対処する必要があります。さらに、生成コードシーケンスはかなり複雑になります。つまり、標準でない呼び出しからの戻りシーケンスとそれに続く分岐です。

だから私の理論は、JITコンパイラの作成者は、より複雑な最適化では効果があるとは考えていなかったということです。ほとんどの人にそのようなJavaコードを書かないでください。彼らはおそらく正しいです。


  • 少なくとも数秒間実行することで、ウォームアップ効果が考慮されると思います。 -XX:+ PrintCompilationによって報告された最後のコンパイルは、Throw.mainから約195msの全体実行までです。 - sigynsurfeit
  • JITコンパイル済みコードが実際に実行されたという証拠はありますか?私が示唆したようにあなたがベンチマークを書いたならば、それは疑いの余地がないでしょう。 - Stephen C

リンクされた質問


関連する質問

最近の質問