0

可能重复:
Java 异常有多慢?

以下两个程序的运行时间大致相同:

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; 回声 $x; 时间 java $x; 完毕
休息
1035892632、1557724831、520446316

真正的 0m10.733s
用户 0m10.719s
系统 0m0.016s
尝试
1035892632、1557724831、520446316

真正的 0m11.218s
用户 0m11.204s
系统 0m0.017s

但是接下来的两个程序所花费的时间是相对不同的:

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; 回声 $x; 时间 java $x; 完毕
返回
84227391, 1000000

实际0m2.437s
用户 0m2.429s
系统 0m0.017s
扔
84227391, 1000000

真正的 0m9.251s
用户 0m9.215s
系统 0m0.014s

我想一个简单的 try/throw/catch 机制看起来有点像一个至少部分尾调用优化的返回(所以它直接知道控制应该返回到哪里(最近的捕获)),但是,当然,JRE 实现做了很多优化。

为什么后者差别很大,而前者差别不大?是因为控制流分析确定前两个程序几乎相同,而实际的 try/throw/catch 特别慢,还是因为 Return 的find展开到某种程度,从而避免了方法调用,而 Throw 的却不能, 或者 ..?谢谢。

编辑:这个问题对我来说似乎与Java 异常有多慢?因为它没有问为什么在与此类似的情况下会有如此大的差异。它还忽略了创建异常对象所花费的时间这一事实(除非fillInStackTrace被覆盖,否则包括遍历堆栈并为其创建数组)。然而,它显然回答了我的部分问题:“是不是因为控制流分析确定前两个程序几乎相同”——尽管在答案中提到堆栈跟踪似乎很奇怪(这可能会使任何实际的 throw/catch 操作相形见绌——除非它进行一些复杂的分析来确定堆栈从未见过,这会使@Stephen 的回答变得奇怪)。

4

1 回答 1

2

您的基准测试没有考虑 JVM 预热效应。因此,您所看到的结果是否真正表明了 try/break/return 在实际程序中的执行情况,这是相当大的疑问。

(您应该在一个方法中声明每个定时测试,并多次调用这些方法。然后丢弃前几次调用的输出......或直到数字稳定......以消除 JIT 的一次性成本编译,类加载等从图中。)


如果你真的想知道发生了什么,你应该让 JIT 编译器转储它为每种情况生成的本机代码。

我怀疑您会发现在第一种情况下,JIT 编译器将方法中的 throw / catch转换为简单的分支指令。在第二种情况下,JIT 编译器很可能正在生成更复杂的代码……大概是因为它不认为这等同于分支。

为什么有区别?好吧,JIT 优化器尝试的每个优化都有成本/收益权衡。JIT 编译器支持的每个新优化都有实施和维护成本。并且在运行时,编译器需要检查它正在编译的代码,看看是否满足优化的先决条件。如果它们是可以执行的优化。否则,JIT 编译器就浪费了时间。

现在在这些示例中,我们有一个异常(在一种情况下)在同一个方法中被抛出和捕获,并且(在另一种情况下)传播到方法调用/返回边界。在前一个示例中,优化的充分前提条件和(可能的)优化代码序列都非常简单。在后一个示例中,优化器必须处理引发和捕获异常的方法位于不同编译单元(因此可能会重新加载)的可能性、覆盖的可能性等等。此外,生成代码序列会明显更复杂......一个非标准的调用返回序列,后跟一个分支。

所以我的理论是 JIT 编译器的作者并不认为更复杂的优化会得到回报。鉴于大多数人不会编写这样的 Java 代码,他们可能是对的。

于 2013-01-27T14:00:19.860 回答