异常的代价有多大?

22

你知道在Java中抛出和处理异常的代价是多么昂贵吗?

我们团队曾经就异常的实际代价进行过多次讨论。有些人尽可能避免使用异常,而有些人则认为使用异常会导致性能损失被夸大了。

今天我在我们的软件中发现了下面这段代码:

private void doSomething()
{
    try
    {
      doSomethingElse();
    }
    catch(DidNotWorkException e)
    {
       log("A Message");
    }
    goOn();
}
private void doSomethingElse()
{
   if(isSoAndSo())
   {
      throw new DidNotWorkException();
   }
   goOnAgain();
}

相对于其他方案,这个方案的性能如何?

private void doSomething()
{
    doSomethingElse();
    goOn();
}
private void doSomethingElse()
{
   if(isSoAndSo())
   {
      log("A Message");
      return;
   }
   goOnAgain();
}

我不想讨论代码美观或其他任何事情,只关心运行时行为!你有真实的经验/测量数据吗?


1
异常昂贵。 - ta.speot.is
10个回答

13

异常并不免费... 因此它们是昂贵的 :-)

书籍《Effective Java》详细介绍了这一点。

  • 第39项:只在异常情况下使用异常。
  • 第40项:将异常用于可恢复的条件。

作者发现,在他的测试用例中,使用异常使代码在他的计算机上以他特定的虚拟机和操作系统组合运行速度变慢了70倍。


1
逻辑谬误 - 他们可能只是很便宜! - alex
不是的...他们不是 :-) (但我必须找到这个引用...) - TofuBeer
异常确实很昂贵。我创建了一个基准测试,其中只有1%的调用会发生异常,这是一个比较现实的情况。即使在这种情况下,性能惩罚也非常高,虽然不是70倍,但仍然非常高。请参见源代码和基准测试结果此处 - Richard Gomes
@RichardGomes 那个链接已经失效了。我非常想看看你的结果。 - Ungeheuer
1
@Ungeheuer:从“Wayback Machine”(网络时光机)上看:https://web.archive.org/web/20130306201020/http://www.jquantlib.org/index.php/Cost_of_Exceptions_in_Java - Richard Gomes

13
抛出异常中最慢的部分是填写堆栈跟踪信息
如果预先创建并重复使用异常,JIT可能会将其优化为"机器级别的转到语句"。
尽管如此,除非您的代码处于非常紧密的循环中,否则差异将是可以忽略的。

1
如果你的异常构造函数调用super(msg, cause, /enableSuppression/false, /writableStackTrace/false),那么你可以创建新的异常而不必存储它。 - giampaolo

9
关于异常的缓慢部分是构建堆栈跟踪(在java.lang.Throwable的构造函数中),它取决于堆栈深度。抛出本身并不慢。
使用异常来信号失败。这样做对性能的影响可以忽略不计,而堆栈跟踪有助于确定失败的原因。
如果您需要将异常用于控制流程(不建议这样做),并且分析表明异常是瓶颈,则创建一个异常子类,覆盖fillInStackTrace(),并使用空实现。或者(或并且)仅实例化一个异常,将其存储在字段中,并始终引发相同的实例。
以下示例演示了异常而没有堆栈跟踪,它向微型基准测试(虽然有缺陷)添加了一种简单方法,在已接受答案中:
public class DidNotWorkException extends Exception {
  public Throwable fillInStackTrace() {
      return this;
  }
}

在 Windows 7 上,使用 JVM 的 -server 模式(版本为 1.6.0_24)运行它会得到以下结果:

Exception:99ms
Boolean:12ms

Exception:92ms
Boolean:11ms

差异在实际应用中足够小,可以忽略不计。

@Dave Jarvis - 如果这是两次独立运行并且结果具有代表性,我不会称900%的执行时间为“几乎没有区别”...如果我们都在编写Hello Worlds,那么可以随意使用异常处理... - im so confused
1
这看起来更像是10的因素,而不是微小的差异。如果您在许多地方使用它,这将会带来很大的影响。 - Arne Babenhauserheide

6

我没有仔细阅读异常相关的内容,但是通过对您修改后的代码进行快速测试,我得出结论:异常情况比布尔值情况慢得多。

我的测试结果如下:

Exception:20891ms
Boolean:62ms

从这段代码开始:
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上 - Sun JVM - WinXP SP3 - Intel Centrino Duo T7200 (2.00Ghz, 977Mhz) - 2.00 GB Ram
在我看来,你应该注意到非异常方法不会在doSomethingElse中给出日志错误,而是返回布尔值,以便调用代码可以处理失败。如果有多个可能失败的区域,则可能需要在内部记录错误或抛出异常。

你确定你测试过以下代码吗?private boolean isSoAndSo() { return false; }如果是这样,那么我对你的结果感到惊讶。 - Kai Huppmann
天啊,你知道吗,我今天真是倒霉透了 :( 不过我的测试结果是正确的,如果没有抛出异常,速度会快两倍。 - Henry B
没问题,感谢您在这个问题上的所有工作! - Kai Huppmann
没问题,很高兴能帮到你。 - Henry B
1
这个比较是不公平的,因为a)doSomethingElse()中的“if”会被任何好的编译器优化掉,b)即使它没有被优化掉,由于现代CPU的分支预测功能,它也不会引起注意。通常情况下,条件大多数时候都是false,只有在特殊情况下才为true。CPU的分支预测期望条件评估为false,并加载else分支的指令,但当它发现条件为true(即特殊情况)时,它必须清空其流水线并加载then分支语句,这可能会耗费几十个CPU周期。 - Stefan Majewsky

5
这是一个非常与JVM相关的问题,所以你不应该盲目相信任何建议,而应该在自己的情况下进行实际测量。可以很容易地创建一个"抛出一百万个异常并打印System.currentTimeMillis的差异",以获得大致想法。
对于您列出的代码片段,我个人会要求原始作者彻底记录为什么在此处使用异常抛出,因为这并不是"最少意外"的路径,这对以后维护非常重要。
(每当您以复杂的方式做某事时,您都会导致读者不必要地进行工作,以理解为什么要像那样做,而不是通常的方式 - 作者必须仔细解释为什么要这样做,因为一定有原因)。
异常是一个非常有用的工具,但只有在必要时才应使用 :)

1
在Java 1.4中,try-catch块存在巨大的性能问题(大多数已在后续版本中得到纠正),其中+1适用于JVM特定情况。 - cletus
+1 适用于JVM,并实际测试它。Java在速度方面已经走了很长的路。 - Kiv

3

我没有具体的测量数据,但抛出异常的代价更高。

好的,这是一个关于.NET框架的链接,但我认为Java也适用:

异常和性能

话虽如此,当适当时,你不应该犹豫使用它们。也就是说:不要将它们用于流程控制,而是在发生了一些异常情况时使用它们;即发生了一些你没有预料到的事情。


完全正确--如果你只在特殊情况下使用异常,那么就没问题了。如果你每秒钟抛出多个异常,它会减慢你的速度,因为它们并不是为这样的流程控制而设计的。 - ojrac
嗯,为什么要降级?如果一个回答被降级,你应该解释原因。 - Frederik Gheysels

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

3
感谢大家的回复。
我最终采用了Thorbjørn的建议,并编写了一个小测试程序,自己测量性能。结果是:两种变体(在性能方面)没有区别。
尽管我没有询问代码美学或其他问题,例如异常的意图等等,但你们大多数人也涉及了这个主题。但实际情况并不总是那么清晰......在考虑的情况下,代码诞生于很久以前,当时抛出异常的情况似乎是异常的情况。今天,库的使用方式和应用程序的使用方式发生了变化,测试覆盖率不是很好,但代码仍然可以正常工作,只是速度有点慢(这就是为什么我要求性能!)。在这种情况下,我认为应该有一个很好的理由从A变成B,而这个理由,在我看来,不能是“异常不是为此而生的!”。
结果证明,日志记录(“A message”)与其他所有操作相比非常昂贵,因此我想去掉它。
编辑:
测试代码与原帖中的代码完全相同,在一个循环中调用名为testPerfomance()的方法,该循环被System.currentTimeMillis()调用包围以获取执行时间......但是:
我现在已经审查了测试代码,关闭了所有其他内容(日志语句),并循环了比以前多100倍,结果发现如果使用B而不是原始帖子中的A,则在一百万次调用时可以节省4.7秒。如Ron所说,fillStackTrace是最昂贵的部分(+1),如果您覆盖它,则可以节省几乎相同的时间(4.5秒)(在您不需要它的情况下)。总的来说,在我的情况下,这仍然是一个几乎为零的差异,因为代码每小时被调用1000次,并且测量显示我可以在那段时间内节省4.5毫秒......
因此,我上面的第一个答案部分有点误导人,但我所说的关于平衡重构的成本效益的内容仍然是正确的。

在你的测试中,异常被抛出的频率有多高?每次还是很少次? - TofuBeer
你能发布一下你测试的代码吗?还有你使用的JVM和操作系统是什么?(我很难相信时间为零或非常小,因为大多数JVM供应商不会费心优化异常情况)。 - TofuBeer
哦...如果您在生产代码中看不到速度变化,那么为什么每次调用方法时都要抛出异常呢?另外,它被调用的频率有多高(如果不是很频繁,则时间不会很长)? - TofuBeer
除非你在运行了5秒以上(并执行了一百万次或更多次迭代)的循环中进行了测试,否则你没有正确地测试这个。搜索SO以获取有关编写秒表基准测试的建议。 - Lawrence Dol
每小时1000次绝对不会引起注意。如果你读过《Effective Java》书中的例子(链接指向Google Books网站,因此这是原文),你会看到一个更为病态的版本(有人在生产代码中使用-呃)。 - TofuBeer
1
我认为你可以避免覆盖fillStackTrace方法:https://dev59.com/XWYq5IYBdhLWcg3wbgHk#14491135 - giampaolo

0
假设在执行语句1和2时不会发生异常。这两个示例代码之间是否存在任何性能损失?
如果没有,那么如果DoSomething()方法必须执行大量的工作(调用其他方法等),情况会如何?
1:
try
{
   DoSomething();
}
catch (...)
{
   ...
}

2:

DoSomething();

在Java中,try是免费的-throw是需要代价的(虽然try并不完全免费...但让我们忘记我说过的吧 :-) 因此,如果异常没有被抛出,那么就不会有速度损失。逻辑的一部分是throw不应该用于流程控制,因此它不是优化的-它是为异常情况而不是常见情况而设计的。 - TofuBeer

0

我认为你的问题有点偏离了。异常是设计用来表示 特殊情况,以及这些情况下的 程序流程机制。所以你应该要问的问题是: 代码逻辑是否需要使用异常。

通常,异常被设计得足够好,可以在它们预期的使用中发挥作用。如果它们被用在瓶颈的地方,那么首先,这可能意味着它们仅仅是全面使用了 “错误的东西” - 即底层问题实际上是一个程序 设计 问题,而不是 性能 问题。

相反,如果异常似乎被用于了 “正确的事情”,那么这可能意味着它也能够良好的执行。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接