How expensive are Exceptions
你知道Java中异常抛出和处理有多昂贵吗?
我们对团队中异常的实际成本进行了几次讨论。有些人尽可能避免使用异常,有些人说通过使用异常而导致的性能损失被高估了。
今天我在我们的软件中发现了以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | private void doSomething() { try { doSomethingElse(); } catch(DidNotWorkException e) { log("A Message"); } goOn(); } private void doSomethingElse() { if(isSoAndSo()) { throw new DidNotWorkException(); } goOnAgain(); } |
这个的性能和
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | private void doSomething() { doSomethingElse(); goOn(); } private void doSomethingElse() { if(isSoAndSo()) { log("A Message"); return; } goOnAgain(); } |
我不想讨论代码美学或任何东西,它只是关于运行时行为!你有真实的经验/测量方法吗?
引发异常的最慢部分是填充堆栈跟踪。
如果预先创建异常并重新使用它,那么JIT可能会将其优化到"机器级GoTo"。
所说的一切,除非问题中的代码处于一个非常紧密的循环中,否则差异将可以忽略不计。
例外不自由…所以它们很贵:—)
《有效Java》一书详细介绍了这一点。
- 第39项仅在特殊情况下使用例外情况。
- 第40项可恢复条件的使用例外
作者发现异常导致代码在他的机器上使用特定的VM和OS组合进行测试时慢了70倍。
异常的缓慢部分是建立堆栈跟踪(在
对信号故障使用异常。然后性能影响可以忽略不计,堆栈跟踪有助于确定故障原因。
如果您需要控制流的异常(不推荐),并且分析显示异常是瓶颈,那么创建一个异常子类,用一个空的实现覆盖
下面通过在接受的答案中向微基准(尽管有缺陷)添加一个简单的方法来演示没有堆栈跟踪的异常:
1 2 3 4 5 |
在
这种差别很小,在实践中是可以忽略的。
我没有费心去读异常,但是用你的一些修改过的代码做了一个非常快速的测试,我得出结论,异常情况比布尔情况慢得多。
我得到了以下结果:
从这个代码:
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 37 38 39 40 41 42 43 | public class Test { public static void main(String args[]) { Test t = new Test(); t.testException(); t.testBoolean(); } public void testException() { long start = System.currentTimeMillis(); for(long i = 0; i <= 10000000L; ++i) doSomethingException(); System.out.println("Exception:" + (System.currentTimeMillis()-start) +"ms"); } public void testBoolean() { long start = System.currentTimeMillis(); for(long i = 0; i <= 10000000L; ++i) doSomething(); System.out.println("Boolean:" + (System.currentTimeMillis()-start) +"ms"); } private void doSomethingException() { try { doSomethingElseException(); } catch(DidNotWorkException e) { //Msg } } private void doSomethingElseException() throws DidNotWorkException { if(!isSoAndSo()) { throw new DidNotWorkException(); } } private void doSomething() { if(!doSomethingElse()) ;//Msg } private boolean doSomethingElse() { if(!isSoAndSo()) return false; return true; } private boolean isSoAndSo() { return false; } public class DidNotWorkException extends Exception {} } |
我愚蠢地没有很好地阅读我的代码,以前有一个错误在里面(如何防腐),如果有人可以三倍检查这个代码,我会非常重视它,以防我会变老。
我的规格是:
- 编译并在1.5.0_16上运行
- 前两副图中
- Win XP SP3
- Intel Centrino Duo T7200(2.00GHz,977MHz)
- 2 GB RAM
在我看来,您应该注意到非异常方法不会在dosomethingelse中给出日志错误,而是返回一个布尔值,以便调用代码能够处理失败。如果有多个区域可能失败,那么可能需要在其中记录错误或引发异常。
这本质上是针对JVM的,所以您不应该盲目地信任给出的任何建议,而应该根据您的情况来衡量。创建一个"抛出一百万个异常并打印出system.currentTimeMillis的差异"来得到一个大致的想法应该不难。
对于您列出的代码片段,我个人要求原始作者彻底记录他为什么在这里使用异常抛出,因为它不是"最不意外的路径",这对于以后维护它至关重要。
(每当你做错综复杂的事情时,你都会让读者做一些不必要的工作,以便理解你为什么这样做而不是通常的方式——在我看来,作者必须仔细解释为什么这样做,因为一定有原因。
异常是一个非常非常有用的工具,但只应在必要时使用:)
感谢您的所有回复。
我终于跟着索比了?注册护士的建议和写了一个小测试程序,衡量自己的表现。结果是:两个变体之间没有区别(在性能方面)。
尽管我没有问过代码美学或其他问题,例如,异常的意图是什么等等,但你们中的大多数人也谈到了这个话题。但事实上事情并不总是那么清楚…在正在考虑的情况下,代码是很久以前诞生的,当时抛出异常的情况似乎是一个异常情况。今天,库的使用方式不同,不同应用程序的行为和使用方式也发生了变化,测试覆盖率不太好,但代码仍然可以完成它的工作,只是速度太慢了一点(这就是为什么我要求性能的原因)。!!)在这种情况下,我认为应该有一个很好的理由将A改为B,在我看来,这不能是"这不是例外!".
结果证明日志("消息")非常昂贵(与其他所有发生的事情相比),所以我想,我会解决这个问题。
编辑:
测试代码与原始post中的测试代码完全相同,在一个循环中由方法
我现在查看了测试代码,打开了所有其他内容(日志语句),比以前循环了100次,结果发现,当使用B而不是原始日志中的A时,您节省了4.7秒的时间来处理一百万个调用。正如罗恩所说,
所以,我上面的第一部分回答有点误导,但是我所说的关于平衡重构的成本效益的话仍然是正确的。
我认为,如果我们坚持在需要的地方使用异常(异常情况),那么好处远远超过您可能支付的任何性能惩罚。我认为可能,因为成本实际上是运行应用程序中抛出异常的频率的函数。
在您给出的示例中,失败似乎不是意外的或灾难性的,因此该方法实际上应该返回一个bool来表示其成功状态,而不是使用异常,从而使它们成为常规控制流的一部分。
在我参与过的几个性能改进工作中,异常的成本相当低。您将花费更多的时间来改进常见的、高度重复的操作的复杂性。
我没有真正的度量标准,但是抛出一个异常更昂贵。
这是一个关于.NETFramework的链接,但我认为同样适用于Java:
例外和性能
也就是说,在适当的时候,你应该毫不犹豫地使用它们。也就是说:不要将它们用于流控制,而是在发生异常情况时使用它们;一些您不希望发生的情况。
假设在尝试执行语句1和2时不会发生异常。这两个示例代码之间是否有性能匹配?
如果不是,那么如果
1:
1 2 3 4 5 6 7 8 | try { DoSomething(); } catch (...) { ... } |
2:
1 | DoSomething(); |
我觉得你问这个的角度有点不对。异常被设计用来表示异常情况,并作为这些情况的程序流机制。因此,您应该问的问题是,代码的"逻辑"是否需要异常。
例外情况通常设计为在预期用途中表现良好。如果它们的使用方式是一个瓶颈,那么最重要的是,这可能表明它们只是被用于"错误的事情",完全停止——也就是说,您潜在的是一个程序设计问题,而不是一个性能问题。
相反,如果异常看起来是"用于正确的事情",那么这可能意味着它也可以正常运行。