问:Java中的异常处理真的很慢吗?

传统观点以及大量谷歌结果表明,不应该将异常逻辑用于Java中的正常程序流。通常会给出两个原因,

它真的很慢——甚至比普通代码慢一个数量级(给出的原因各不相同),

and

它很混乱,因为人们只希望在异常代码中处理错误。

这个问题是关于第一条的。

As an example, this page describes Java exception handling as "very slow" and relates the slowness to the creation of the exception message string - "this string is then used in creating the exception object that is thrown. This is not fast." The article Effective Exception Handling in Java says that "the reason for this is due to the object creation aspect of exception handling, which thereby makes throwing exceptions inherently slow". Another reason out there is that the stack trace generation is what slows it down.

My testing (using Java 1.6.0_07, Java HotSpot 10.0, on 32 bit Linux), indicates that exception handling is no slower than regular code. I tried running a method in a loop that executes some code. At the end of the method, I use a boolean to indicate whether to return or throw. This way the actual processing is the same. I tried running the methods in different orders and averaging my test times, thinking it may have been the JVM warming up. In all my tests, the throw was at least as fast as the return, if not faster (up to 3.1% faster). I am completely open to the possibility that my tests were wrong, but I haven't seen anything out there in the way of the code sample, test comparisons, or results in the last year or two that show exception handling in Java to actually be slow.

引导我走上这条路的是我需要使用的一个API,它将抛出异常作为正常控制逻辑的一部分。我想纠正它们的用法,但现在我可能做不到。我是否应该赞美他们的前瞻思维?

在论文《即时编译中的高效Java异常处理》中,作者建议,即使没有抛出异常,仅异常处理程序的存在就足以阻止JIT编译器正确优化代码,从而降低代码的速度。我还没有测试过这个理论。


当前回答

为什么异常回报率会比正常回报率慢呢?

只要不将堆栈跟踪输出到终端,将其保存到一个文件或类似的文件中,catch块就不会比其他代码块做更多的工作。所以,我无法想象为什么“throw new my_cool_error()”应该这么慢。

好问题,我期待关于这个话题的进一步信息!

其他回答

比较一下,假设是Integer。将parseInt转换为以下方法,该方法在不可解析数据的情况下只返回默认值,而不会抛出异常:

  public static int parseUnsignedInt(String s, int defaultValue) {
    final int strLength = s.length();
    if (strLength == 0)
      return defaultValue;
    int value = 0;
    for (int i=strLength-1; i>=0; i--) {
      int c = s.charAt(i);
      if (c > 47 && c < 58) {
        c -= 48;
        for (int j=strLength-i; j!=1; j--)
          c *= 10;
        value += c;
      } else {
        return defaultValue;
      }
    }
    return value < 0 ? /* übergebener wert > Integer.MAX_VALUE? */ defaultValue : value;
  }

只要您将这两种方法应用于“有效”数据,它们将以大致相同的速率工作(即使Integer。parseInt设法处理更复杂的数据)。但是当您试图解析无效数据时(例如解析“abc”1.000.000次),性能上的差异应该是至关重要的。

Aleksey Shipilëv做了一个非常彻底的分析,他在各种条件组合下对Java异常进行了基准测试:

新创建的异常vs预先创建的异常 启用与禁用堆栈跟踪 请求的堆栈跟踪vs从未请求的堆栈跟踪 在顶层捕获vs在每一层重新抛出vs在每一层被链接/包裹 不同级别的Java调用堆栈深度 无内联优化vs极端内联vs默认设置 用户定义字段读与不读

他还将它们与在不同错误频率级别检查错误代码的性能进行了比较。

结论(逐字摘自他的帖子)如下:

Truly exceptional exceptions are beautifully performant. If you use them as designed, and only communicate the truly exceptional cases among the overwhelmingly large number of non-exceptional cases handled by regular code, then using exceptions is the performance win. The performance costs of exceptions have two major components: stack trace construction when Exception is instantiated and stack unwinding during Exception throw. Stack trace construction costs are proportional to stack depth at the moment of exception instantiation. That is already bad because who on Earth knows the stack depth at which this throwing method would be called? Even if you turn off the stack trace generation and/or cache the exceptions, you can only get rid of this part of the performance cost. Stack unwinding costs depend on how lucky we are with bringing the exception handler closer in the compiled code. Carefully structuring the code to avoid deep exception handlers lookup is probably helping us get luckier. Should we eliminate both effects, the performance cost of exceptions is that of the local branch. No matter how beautiful it sounds, that does not mean you should use Exceptions as the usual control flow, because in that case you are at the mercy of optimizing compiler! You should only use them in truly exceptional cases, where the exception frequency amortizes the possible unlucky cost of raising the actual exception. The optimistic rule-of-thumb seems to be 10^-4 frequency for exceptions is exceptional enough. That, of course, depends on the heavy-weights of the exceptions themselves, the exact actions taken in exception handlers, etc.

结果是,当没有抛出异常时,您不会付出代价,因此当异常条件足够罕见时,异常处理比每次都使用if更快。这篇文章的全文非常值得一读。

HotSpot非常能够删除系统生成的异常代码,只要它是内联的。但是,显式创建的异常和其他未删除的异常要花费大量时间来创建堆栈跟踪。重写fillInStackTrace以查看这会如何影响性能。

不幸的是,我的回答太长了,不能在这里发表。所以让我在这里总结一下,并向你推荐http://www.fuwjax.com/how-slow-are-java-exceptions/以获得更具体的细节。

这里真正的问题不是“与“从未失败的代码”相比,“将失败报告为异常”的速度有多慢?”,正如人们所接受的回答可能会让你相信的那样。相反,问题应该是“与其他方式报告的失败相比,‘作为异常报告的失败’有多慢?”通常,报告失败的另外两种方法是使用哨兵值或使用结果包装器。

哨兵值是在成功情况下返回一个类,在失败情况下返回另一个类的尝试。你几乎可以把它看作是返回一个异常而不是抛出一个异常。这需要一个与success对象共享的父类,然后执行“instanceof”检查和几个类型转换来获得成功或失败的信息。

事实证明,冒着类型安全的风险,Sentinel值比异常快,但仅快大约2倍。现在,这可能看起来很多,但2倍只包括实现差异的成本。实际上,这个因素要低得多,因为我们可能失败的方法要比本页其他地方示例代码中的几个算术运算符有趣得多。

另一方面,结果包装器根本不牺牲类型安全。它们将成功和失败信息包装在单个类中。因此,它们提供了一个“isSuccess()”来代替“instanceof”,并为成功和失败对象提供了getter。但是,结果对象大约比使用异常慢2倍。事实证明,每次创建一个新的包装器对象比有时抛出异常要昂贵得多。

最重要的是,异常是语言提供的一种指示方法可能失败的方式。没有其他方法可以仅从API判断哪些方法总是(大部分)工作,哪些方法报告失败。

异常比哨兵更安全,比结果对象更快,并且比两者都不那么令人惊讶。我并不是建议用try/catch替换if/else,但是异常是报告失败的正确方式,即使在业务逻辑中也是如此。

也就是说,我想指出的是,我遇到的两种最常见的实质上影响性能的方法是创建不必要的对象和嵌套循环。如果可以在创建异常和不创建异常之间选择,请不要创建异常。如果要在有时创建异常或始终创建另一个对象之间做出选择,那么就创建异常。

为什么异常回报率会比正常回报率慢呢?

只要不将堆栈跟踪输出到终端,将其保存到一个文件或类似的文件中,catch块就不会比其他代码块做更多的工作。所以,我无法想象为什么“throw new my_cool_error()”应该这么慢。

好问题,我期待关于这个话题的进一步信息!