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; 時間 Java $x; 終わり
壊す
1035892632、1557724831、520446316

実質 0m10.733s
ユーザー 0m10.719s
システム 0m0.016s
試す
1035892632、1557724831、520446316

実質 0 分 11.218 秒
ユーザー 0m11.204s
システム 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; 時間 Java $x; 終わり
戻る
84227391、1000000

実質 0m2.437s
ユーザー 0m2.429s
システム 0m0.017s
投げる
84227391、1000000

実質 0 分 9.251 秒
ユーザー 0 分 9.215 秒
システム 0m0.014s

単純な try/throw/catch メカニズムは、少なくとも部分的に末尾呼び出しが最適化された戻り値のように見えると思います (そのため、制御がどこに戻る必要があるか (最も近いキャッチ) が直接わかります)。もちろん、JRE の実装では多くの最適化が行われます。

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

編集: この質問は、Java の例外はどれくらい遅いですか? とは異なるようです。これは、このようなケースでなぜこれほど大きな違いがあるのか​​を尋ねていないからです。また、例外オブジェクトの作成に時間がかかるという事実も無視します (fillInStackTraceオーバーライドされない限り、スタックを走査してその配列を作成するなどの処理が含まれます)。しかし、それはどうやら私の質問の一部に答えているようです。おそらく、実際のスロー/キャッチ操作を矮小化するでしょう—複雑な分析を行ってスタックが決して見られないと判断しない限り、@Stephenの答えは奇妙になります)。

4

1 に答える 1

2

ベンチマークは、JVM ウォームアップ効果を考慮していません。したがって、表示されている結果が、実際のプログラムでの try / break / return の実行方法を正しく示しているかどうかについては、かなりの疑いがあります。

(メソッドで各時間指定テストを宣言し、メソッドを何度も呼び出す必要があります。次に、最初の数回の呼​​び出しからの出力を破棄します...または数が安定するまで...JITの1回限りのコストを排除しますコンパイル、クラスのロードなどは図から。)


何が起こっているのかを本当に知りたい場合は、JIT コンパイラーに、それぞれのケースで生成されたネイティブ コードをダンプさせる必要があります。

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

違いはなぜですか?まあ、JIT オプティマイザーによって試行される各最適化には、コストと利益のトレードオフがあります。JIT コンパイラーがサポートする新しい最適化にはそれぞれ、実装と保守のコストがかかります。また、実行時に、コンパイラはコンパイル中のコードを調べて、最適化の前提条件が満たされているかどうかを確認する必要があります。その場合、最適化を実行できます。そうしないと、JIT コンパイラーは時間を無駄にします。

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

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

于 2013-01-27T14:00:19.860 に答える