Java异常有多慢?
问题:Java中的异常处理实际上很慢吗?
传统观点以及大量的Google结果都表明,特殊的逻辑不应该用于Java中的正常程序流程。 通常有两个原因,
和
这个问题是关于#1的。
举例来说,这个页面将Java异常处理描述为“非常慢”,并将慢速与创建异常消息字符串联系起来 - “然后将此字符串用于创建抛出的异常对象,但这并不快。 Java中的有效异常处理(The Effective Exception Handling)在Java中表示,“其原因是由于异常处理的对象创建方面的原因,从而导致异常抛出异常缓慢”。 另一个原因是堆栈跟踪的生成会降低速度。
我的测试(在32位Linux上使用Java 1.6.0_07,Java HotSpot 10.0)表明异常处理不会比普通代码慢。 我试着在执行一些代码的循环中运行一个方法。 在该方法结束时,我使用布尔值来指示是返回还是抛出。 这样实际的处理过程是一样的。 我试着以不同的顺序运行这些方法并平均测试时间,认为它可能是JVM热身。 在我的所有测试中,投掷的速度至少与回归速度一样快,如果不快的话(速度快3.1%)。 我对我的测试错误的可能性完全保持开放,但在代码示例,测试比较或上一两年的结果中,我没有看到任何东西,它们显示Java中的异常处理实际上很慢。
让我走这条路的是我需要使用的API,它将异常作为正常控制逻辑的一部分。 我想纠正他们的用法,但现在我可能无法做到。 我是否会不得不赞扬他们的前瞻性思维?
在即时编译中的Efficient Java异常处理文章中,作者指出,即使没有抛出任何异常,单独存在异常处理程序也足以阻止JIT编译器正确优化代码,从而减慢其速度。 我还没有测试过这个理论。
这取决于如何实施例外。 最简单的方法是使用setjmp和longjmp。 这意味着CPU的所有寄存器都被写入堆栈(这已经花费了一些时间),并且可能还需要创建其他一些数据......所有这些都已经在try语句中发生了。 throw语句需要展开堆栈并恢复所有寄存器的值(以及VM中可能的其他值)。 所以尝试和抛出同样很慢,而且这很慢,但是如果没有抛出异常,在大多数情况下退出try块不会有任何时间(因为如果方法存在,所有东西都放在堆栈上自动清除)。
Sun和其他人认识到,这可能不是最理想的,当然VM在一段时间内会越来越快。 还有另外一种方法来实现异常,这使得尝试自己闪电般快速(实际上根本没有什么事情发生,一般情况下 - 所有需要发生的事情在虚拟机加载类时都已经完成),并且它使得投掷速度不尽如人意。 我不知道哪个JVM使用这种新的更好的技术...
...但是你是用Java写的,所以你以后的代码只能在一个特定系统上的一个JVM上运行? 因为如果它可能运行在任何其他平台或任何其他JVM版本(可能是任何其他供应商)上,谁说他们也使用快速实现? 最快的一个比慢一个更复杂,不容易在所有系统上使用。 你想保持便携? 那么不要依赖快速的例外。
这对你在try块中做的事情也有很大的影响。 如果你打开一个try块,并且从未在这个try块中调用任何方法,那么try块将会超快,因为JIT可以实际上像一个简单的goto一样处理一个throw。 它不需要保存堆栈状态,也不需要在抛出异常时展开堆栈(它只需跳转到catch处理程序)。 但是,这通常不是你通常做的。 通常你打开一个try块,然后调用一个可能抛出异常的方法,对吧? 即使你只是在你的方法中使用try块,这种方法是什么样的,不会调用任何其他方法? 它只是计算一个数字? 那么你需要什么例外? 有更多优雅的方式来规范程序流程。 除了简单的数学以外,其他任何东西都不得不调用外部方法,这已经破坏了本地try块的优点。
请参阅以下测试代码:
public class Test {
int value;
public int getValue() {
return value;
}
public void reset() {
value = 0;
}
// Calculates without exception
public void method1(int i) {
value = ((value + i) / i) << 1;
// Will never be true
if ((i & 0xFFFFFFF) == 1000000000) {
System.out.println("You'll never see this!");
}
}
// Could in theory throw one, but never will
public void method2(int i) throws Exception {
value = ((value + i) / i) << 1;
// Will never be true
if ((i & 0xFFFFFFF) == 1000000000) {
throw new Exception();
}
}
// This one will regularly throw one
public void method3(int i) throws Exception {
value = ((value + i) / i) << 1;
// i & 1 is equally fast to calculate as i & 0xFFFFFFF; it is both
// an AND operation between two integers. The size of the number plays
// no role. AND on 32 BIT always ANDs all 32 bits
if ((i & 0x1) == 1) {
throw new Exception();
}
}
public static void main(String[] args) {
int i;
long l;
Test t = new Test();
l = System.currentTimeMillis();
t.reset();
for (i = 1; i < 100000000; i++) {
t.method1(i);
}
l = System.currentTimeMillis() - l;
System.out.println(
"method1 took " + l + " ms, result was " + t.getValue()
);
l = System.currentTimeMillis();
t.reset();
for (i = 1; i < 100000000; i++) {
try {
t.method2(i);
} catch (Exception e) {
System.out.println("You'll never see this!");
}
}
l = System.currentTimeMillis() - l;
System.out.println(
"method2 took " + l + " ms, result was " + t.getValue()
);
l = System.currentTimeMillis();
t.reset();
for (i = 1; i < 100000000; i++) {
try {
t.method3(i);
} catch (Exception e) {
// Do nothing here, as we will get here
}
}
l = System.currentTimeMillis() - l;
System.out.println(
"method3 took " + l + " ms, result was " + t.getValue()
);
}
}
结果:
method1 took 972 ms, result was 2
method2 took 1003 ms, result was 2
method3 took 66716 ms, result was 2
try块的减速太小,无法排除混淆因素,如后台进程。 但是捕获块会把所有东西都杀死,让它慢66倍!
正如我所说的,如果将try / catch放在同一个方法(method3)中,结果不会那么糟糕,但这是我不会依赖的特殊JIT优化。 即使使用这种优化,投掷仍然很慢。 所以我不知道你在这里试图做什么,但肯定有比使用try / catch / throw更好的方法。
仅供参考,我扩展了Mecki所做的实验:
method1 took 1733 ms, result was 2
method2 took 1248 ms, result was 2
method3 took 83997 ms, result was 2
method4 took 1692 ms, result was 2
method5 took 60946 ms, result was 2
method6 took 25746 ms, result was 2
前3个与Mecki相同(我的笔记本电脑显然比较慢)。
除了创建一个new Integer(1)
而不是throw new Exception()
之外,method4与method3相同。
method5与method3类似,不同的是它创建new Exception()
而不抛出它。
method6与method3相似,不同之处在于它抛出一个预先创建的异常(一个实例变量),而不是创建一个新的异常。
在Java中,抛出异常的大部分花费是收集堆栈跟踪所花费的时间,这是在创建异常对象时发生的。 抛出异常的实际成本虽然很大,但远低于创建异常的成本。
AlekseyShipilëv做了非常全面的分析,他在各种条件组合下测试了Java异常:
他还将它们与在各种错误频率级别检查错误代码的性能进行比较。
结论(他的帖子逐字引用)是:
真正例外的例外是美丽的表演。 如果按照设计使用它们,并且仅在正规代码处理的绝大多数非例外情况中传达真正的例外情况,那么使用例外就是表现胜利。
异常的性能成本有两个主要组成部分:异常实例化时的堆栈跟踪结构和异常抛出期间的堆栈展开 。
堆栈跟踪构建成本与异常实例化时的堆栈深度成正比 。 这已经很糟糕了,因为地球上的人知道这种投掷方法将被调用的堆栈深度? 即使您关闭堆栈跟踪生成和/或缓存异常,您也只能摆脱这部分性能成本。
堆栈展开成本取决于我们在编译代码中使异常处理程序靠得更近的幸运程度。 仔细构造代码以避免深度异常处理程序查找可能帮助我们获得更多幸运。
如果我们消除这两种影响,例外的性能成本就是本地分支的性能成本。 不管它听起来多么美,这并不意味着你应该使用异常作为通常的控制流程,因为在这种情况下, 你处于优化编译器的摆布! 你只应该在真正的特殊情况下,异常的频率提高摊销实际的异常的可能倒霉的成本使用它们。
乐观的经验法则似乎是10 ^ -4的频率,例外是非常特殊的。 这当然取决于例外本身的沉重程度,异常处理程序中采取的确切行动等。
其结果是,在没有抛出异常,你不付出成本,所以当异常情况是非常罕见的异常处理是比使用更快if
每一次。 全文非常值得一读。
上一篇: How slow are Java exceptions?
下一篇: How do you assert that a certain exception is thrown in JUnit 4 tests?