What are the effects of exceptions on performance in Java?

前端 未结 17 2371
情深已故
情深已故 2020-11-22 01:21

Question: Is exception handling in Java actually slow?

Conventional wisdom, as well as a lot of Google results, says that exceptional logic shouldn\'t be used for n

相关标签:
17条回答
  • 2020-11-22 01:45

    I've extends the answers given by @Mecki and @incarnate, without stacktrace filling for Java.

    With Java 7+, we can use Throwable(String message, Throwable cause, boolean enableSuppression,boolean writableStackTrace). But for Java6, see my answer for this question

    // This one will regularly throw one
    public void method4(int i) throws NoStackTraceThrowable {
        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 NoStackTraceThrowable();
        }
    }
    
    // This one will regularly throw one
    public void method5(int i) throws NoStackTraceRuntimeException {
        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 NoStackTraceRuntimeException();
        }
    }
    
    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++) {
            try {
                t.method4(i);
            } catch (NoStackTraceThrowable e) {
                // Do nothing here, as we will get here
            }
        }
        l = System.currentTimeMillis() - l;
        System.out.println( "method4 took " + l + " ms, result was " + t.getValue() );
    
    
        l = System.currentTimeMillis();
        t.reset();
        for (i = 1; i < 100000000; i++) {
            try {
                t.method5(i);
            } catch (RuntimeException e) {
                // Do nothing here, as we will get here
            }
        }
        l = System.currentTimeMillis() - l;
        System.out.println( "method5 took " + l + " ms, result was " + t.getValue() );
    }
    

    Output with Java 1.6.0_45, on Core i7, 8GB RAM:

    method1 took 883 ms, result was 2
    method2 took 882 ms, result was 2
    method3 took 32270 ms, result was 2 // throws Exception
    method4 took 8114 ms, result was 2 // throws NoStackTraceThrowable
    method5 took 8086 ms, result was 2 // throws NoStackTraceRuntimeException
    

    So, still methods which returns values are faster, compared to methods throwing exceptions. IMHO, we can't design a clear API just using return types for both success & error flows. Methods which throws exceptions without stacktrace are 4-5 times faster than normal Exceptions.

    Edit: NoStackTraceThrowable.java Thanks @Greg

    public class NoStackTraceThrowable extends Throwable { 
        public NoStackTraceThrowable() { 
            super("my special throwable", null, false, false);
        }
    }
    
    0 讨论(0)
  • 2020-11-22 01:45

    Don't know if these topics relate, but I once wanted to implement one trick relying on current thread's stack trace: I wanted to discover the name of the method, which triggered the instantiation inside the instantiated class (yeap, the idea is crazy, I totally gave it up). So I discovered that calling Thread.currentThread().getStackTrace() is extremely slow (due to native dumpThreads method which it uses internally).

    So Java Throwable, correspondingly, has a native method fillInStackTrace. I think that the killer-catch block described earlier somehow triggers the execution of this method.

    But let me tell you another story...

    In Scala some functional features are compiled in JVM using ControlThrowable, which extends Throwable and overrides its fillInStackTrace in a following way:

    override def fillInStackTrace(): Throwable = this
    

    So I adapted the test above (cycles amount are decreased by ten, my machine is a bit slower :):

    class ControlException extends ControlThrowable
    
    class T {
      var value = 0
    
      def reset = {
        value = 0
      }
    
      def method1(i: Int) = {
        value = ((value + i) / i) << 1
        if ((i & 0xfffffff) == 1000000000) {
          println("You'll never see this!")
        }
      }
    
      def method2(i: Int) = {
        value = ((value + i) / i) << 1
        if ((i & 0xfffffff) == 1000000000) {
          throw new Exception()
        }
      }
    
      def method3(i: Int) = {
        value = ((value + i) / i) << 1
        if ((i & 0x1) == 1) {
          throw new Exception()
        }
      }
    
      def method4(i: Int) = {
        value = ((value + i) / i) << 1
        if ((i & 0x1) == 1) {
          throw new ControlException()
        }
      }
    }
    
    class Main {
      var l = System.currentTimeMillis
      val t = new T
      for (i <- 1 to 10000000)
        t.method1(i)
      l = System.currentTimeMillis - l
      println("method1 took " + l + " ms, result was " + t.value)
    
      t.reset
      l = System.currentTimeMillis
      for (i <- 1 to 10000000) try {
        t.method2(i)
      } catch {
        case _ => println("You'll never see this")
      }
      l = System.currentTimeMillis - l
      println("method2 took " + l + " ms, result was " + t.value)
    
      t.reset
      l = System.currentTimeMillis
      for (i <- 1 to 10000000) try {
        t.method4(i)
      } catch {
        case _ => // do nothing
      }
      l = System.currentTimeMillis - l
      println("method4 took " + l + " ms, result was " + t.value)
    
      t.reset
      l = System.currentTimeMillis
      for (i <- 1 to 10000000) try {
        t.method3(i)
      } catch {
        case _ => // do nothing
      }
      l = System.currentTimeMillis - l
      println("method3 took " + l + " ms, result was " + t.value)
    
    }
    

    So, the results are:

    method1 took 146 ms, result was 2
    method2 took 159 ms, result was 2
    method4 took 1551 ms, result was 2
    method3 took 42492 ms, result was 2
    

    You see, the only difference between method3 and method4 is that they throw different kinds of exceptions. Yeap, method4 is still slower than method1 and method2, but the difference is far more acceptable.

    0 讨论(0)
  • 2020-11-22 01:46

    Aleksey Shipilëv did a very thorough analysis in which he benchmarks Java exceptions under various combinations of conditions:

    • Newly created exceptions vs pre-created exceptions
    • Stack trace enabled vs disabled
    • Stack trace requested vs never requested
    • Caught at the top level vs rethrown at every level vs chained/wrapped at every level
    • Various levels of Java call stack depth
    • No inlining optimizations vs extreme inlining vs default settings
    • User-defined fields read vs not read

    He also compares them to the performance of checking an error code at various levels of error frequency.

    The conclusions (quoted verbatim from his post) were:

    1. 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.

    2. The performance costs of exceptions have two major components: stack trace construction when Exception is instantiated and stack unwinding during Exception throw.

    3. 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.

    4. 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.

    5. 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.

    6. 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.

    The upshot is that when an exception isn't thrown, you don't pay a cost, so when the exceptional condition is sufficiently rare exception handling is faster than using an if every time. The full post is very much worth a read.

    0 讨论(0)
  • 2020-11-22 01:46

    My answer, unfortunately, is just too long to post here. So let me summarize here and refer you to http://www.fuwjax.com/how-slow-are-java-exceptions/ for the gritty details.

    The real question here is not "How slow are 'failures reported as exceptions' compared to 'code that never fails'?" as the accepted response might have you believe. Instead, the question should be "How slow are 'failures reported as exceptions' compared to failures reported other ways?" Generally, the two other ways of reporting failures are either with sentinel values or with result wrappers.

    Sentinel values are an attempt to return one class in the case of success and another in the case of failure. You can think of it almost as returning an exception instead of throwing one. This requires a shared parent class with the success object and then doing an "instanceof" check and a couple casts to get the success or failure information.

    It turns out that at the risk of type safety, Sentinel values are faster than exceptions, but only by a factor of roughly 2x. Now, that may seem like a lot, but that 2x only covers the cost of the implementation difference. In practice, the factor is much lower since our methods that might fail are much more interesting than a few arithmetic operators as in the sample code elsewhere in this page.

    Result Wrappers, on the other hand, do not sacrifice type safety at all. They wrap the success and failure information in a single class. So instead of "instanceof" they provide an "isSuccess()" and getters for both the success and failure objects. However, result objects are roughly 2x slower than using exceptions. It turns out that creating a new wrapper object every time is much more expensive than throwing an exception sometimes.

    On top of that, exceptions are the language supplied the way of indicating that a method might fail. There's no other way to tell from just the API which methods are expected to always (mostly) work and which are expected to report failure.

    Exceptions are safer than sentinels, faster than result objects, and less surprising than either. I'm not suggesting that try/catch replace if/else, but exceptions are the right way to report failure, even in the business logic.

    That said, I would like to point out that the two most frequent ways of substantially impacting performance I've run across are creating unnecessary objects and nested loops. If you have a choice between creating an exception or not creating an exception, don't create the exception. If you have a choice between creating an exception sometimes or creating another object all the time, then create the exception.

    0 讨论(0)
  • 2020-11-22 01:47

    FYI, I extended the experiment that Mecki did:

    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
    

    The first 3 are the same as Mecki's (my laptop is obviously slower).

    method4 is identical to method3 except that it creates a new Integer(1) rather than doing throw new Exception().

    method5 is like method3 except that it creates the new Exception() without throwing it.

    method6 is like method3 except that it throws a pre-created exception (an instance variable) rather than creating a new one.

    In Java much of the expense of throwing an exception is the time spent gathering the stack trace, which occurs when the exception object is created. The actual cost of throwing the exception, while large, is considerably less than the cost of creating the exception.

    0 讨论(0)
提交回复
热议问题