关于java:例外有多贵

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倍。


异常的缓慢部分是建立堆栈跟踪(在java.lang.Throwable的构造函数中),这取决于堆栈深度。投入本身并不慢。

对信号故障使用异常。然后性能影响可以忽略不计,堆栈跟踪有助于确定故障原因。

如果您需要控制流的异常(不推荐),并且分析显示异常是瓶颈,那么创建一个异常子类,用一个空的实现覆盖fillInStackTrace()。或者(或者另外)只实例化一个异常,将其存储在一个字段中,并始终抛出同一个实例。

下面通过在接受的答案中向微基准(尽管有缺陷)添加一个简单的方法来演示没有堆栈跟踪的异常:

1
2
3
4
5
public class DidNotWorkException extends Exception {
  public Throwable fillInStackTrace() {
      return this;
  }
}

-server模式(Windows7上的版本1.6.0_24)下使用jvm运行它会导致:

1
2
3
4
5
Exception:99ms
Boolean:12ms

Exception:92ms
Boolean:11ms

这种差别很小,在实践中是可以忽略的。


我没有费心去读异常,但是用你的一些修改过的代码做了一个非常快速的测试,我得出结论,异常情况比布尔情况慢得多。

我得到了以下结果:

1
2
Exception:20891ms
Boolean:62ms

从这个代码:

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中的测试代码完全相同,在一个循环中由方法testPerfomance()调用,该循环由System.currentTimeMillis()调用包围,以获得执行时间…但是:

我现在查看了测试代码,打开了所有其他内容(日志语句),比以前循环了100次,结果发现,当使用B而不是原始日志中的A时,您节省了4.7秒的时间来处理一百万个调用。正如罗恩所说,fillStackTrace是最昂贵的部分(+1),如果你覆盖它(在这种情况下你不需要它,像我一样),你可以节省几乎相同的(4.5秒)。总之,在我的例子中,它仍然是一个几乎为零的差异,因为代码被称为每小时1000次,而且测量结果显示我可以在这段时间内节省4.5毫秒…

所以,我上面的第一部分回答有点误导,但是我所说的关于平衡重构的成本效益的话仍然是正确的。


我认为,如果我们坚持在需要的地方使用异常(异常情况),那么好处远远超过您可能支付的任何性能惩罚。我认为可能,因为成本实际上是运行应用程序中抛出异常的频率的函数。
在您给出的示例中,失败似乎不是意外的或灾难性的,因此该方法实际上应该返回一个bool来表示其成功状态,而不是使用异常,从而使它们成为常规控制流的一部分。
在我参与过的几个性能改进工作中,异常的成本相当低。您将花费更多的时间来改进常见的、高度重复的操作的复杂性。


我没有真正的度量标准,但是抛出一个异常更昂贵。

这是一个关于.NETFramework的链接,但我认为同样适用于Java:

例外和性能

也就是说,在适当的时候,你应该毫不犹豫地使用它们。也就是说:不要将它们用于流控制,而是在发生异常情况时使用它们;一些您不希望发生的情况。


假设在尝试执行语句1和2时不会发生异常。这两个示例代码之间是否有性能匹配?

如果不是,那么如果DoSomething()方法必须做大量的工作(调用其他方法的负载等),该怎么办?

1:

1
2
3
4
5
6
7
8
try
{
   DoSomething();
}
catch (...)
{
   ...
}

2:

1
DoSomething();


我觉得你问这个的角度有点不对。异常被设计用来表示异常情况,并作为这些情况的程序流机制。因此,您应该问的问题是,代码的"逻辑"是否需要异常。

例外情况通常设计为在预期用途中表现良好。如果它们的使用方式是一个瓶颈,那么最重要的是,这可能表明它们只是被用于"错误的事情",完全停止——也就是说,您潜在的是一个程序设计问题,而不是一个性能问题。

相反,如果异常看起来是"用于正确的事情",那么这可能意味着它也可以正常运行。