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实现做了很多优化。
为什么后者有很大的区别而不是前者?这是因为控制流分析确定前两个程序基本相同,而实际的尝试/抛出/捕获速度特别慢,还是因为返回的
编辑:这个问题对我来说,Java异常有多慢?因为它并没有问为什么在类似的情况下会有如此大的差异。它还忽略了创建异常对象所花费的时间(除非重写
您的基准不考虑JVM预热效果。因此,有相当大的疑问,您所看到的结果确实表明,在真正的程序中,try/break/return将如何执行。
(您应该在一个方法中声明每个定时测试,并多次调用这些方法。然后放弃前几个调用的输出…或者直到数字稳定下来…从图中消除JIT编译、类加载等一次性成本。)
如果您真的想知道发生了什么,您应该让JIT编译器转储它为每种情况生成的本机代码。
我怀疑您会发现在第一种情况下,JIT编译器将方法中的throw/catch转换为简单的分支指令。在第二种情况下,JIT编译器可能正在生成更复杂的代码…大概是因为它不承认这等同于一个分支。
为什么不同?好吧,对于JIT优化器尝试的每个优化都有一个成本/收益权衡。JIT编译器支持的每个新优化都有一个实现和维护成本。在运行时,编译器需要检查它正在编译的代码,以查看是否满足优化的前提条件。如果是,则可以执行优化。否则,JIT编译器浪费了时间。
在这些示例中,我们有一个异常(在一种情况下)在同一个方法中被抛出和捕获,并且(在另一种情况下)在方法调用/返回边界上传播。在前一个例子中,优化的充分前提条件和(可能的)优化的代码序列都非常简单。在后一个示例中,优化器必须处理引发和捕获异常的方法在不同的编译单元(因此可以重新加载)中的可能性、重写的可能性等等。此外,生成的代码序列将明显更加复杂…从调用序列返回的非标准返回,后跟一个分支。
所以我的理论是,JIT编译器的作者认为更复杂的优化不会有回报。考虑到大多数人没有像那样编写Java代码,他们可能是对的。