关于基准测试:为什么try / catch与其他控制形式的性能差异在Java中流动?

Why the performance discrepancy in try/catch vs. other forms of control flow in Java?

本问题已经有最佳答案,请猛点这里访问。

Possible Duplicate:
How slow are Java exceptions?

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
$ 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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
$ 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机制看起来有点像至少部分尾部调用优化的返回(所以直接知道控制应该返回到哪里(最近的捕获)),但是,当然,JRE实现做了很多优化。

为什么后者有很大的区别而不是前者?这是因为控制流分析确定前两个程序基本相同,而实际的尝试/抛出/捕获速度特别慢,还是因为返回的find展开到某个级别,从而避免了方法调用,而抛出的则不能,还是…?谢谢。

编辑:这个问题对我来说,Java异常有多慢?因为它并没有问为什么在类似的情况下会有如此大的差异。它还忽略了创建异常对象所花费的时间(除非重写fillInStackTrace包括遍历堆栈和为其创建数组之类的内容)。然而,它显然回答了我问题的一部分:"这是因为控制流分析确定前两个程序几乎是相同的吗?"尽管奇怪的是,答案中提到了堆栈跟踪(这可能会使任何实际的抛出/捕获操作相形见绌,除非它执行了一些复杂的分析以确定从未见过堆栈,这将使@stephen's answer奇数)。


您的基准不考虑JVM预热效果。因此,有相当大的疑问,您所看到的结果确实表明,在真正的程序中,try/break/return将如何执行。

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

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

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

为什么不同?好吧,对于JIT优化器尝试的每个优化都有一个成本/收益权衡。JIT编译器支持的每个新优化都有一个实现和维护成本。在运行时,编译器需要检查它正在编译的代码,以查看是否满足优化的前提条件。如果是,则可以执行优化。否则,JIT编译器浪费了时间。

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

所以我的理论是,JIT编译器的作者认为更复杂的优化不会有回报。考虑到大多数人没有像那样编写Java代码,他们可能是对的。