ChatGPT解决这个技术问题 Extra ChatGPT

Which part of throwing an Exception is expensive?

In Java, using throw/catch as a part of logic when there's not actually an error is generally a bad idea (in part) because throwing and catching an exception is expensive, and doing it many times in a loop is usually far slower than other control structures which don't involve throwing exceptions.

My question is, is the cost incurred in the throw/catch itself, or when creating the Exception object (since it gets a lot of runtime information including the execution stack)?

In other words, if I do

Exception e = new Exception();

but don't throw it, is that most of the cost of throwing, or is the throw + catch handling what's costly?

I'm not asking whether putting code in a try/catch block adds to the cost of executing that code, I'm asking whether catching the Exception is the expensive part, or creating (calling the constructor for) the Exception is the expensive part.

Another way of asking this is, if I made one instance of Exception and threw and caught it over and over, would that be significantly faster than creating a new Exception every time I throw?

I believe it is filling in and populating the stack trace.
"if I made one instance of Exception and threw and caught it over and over," when exception is created its stacktrace is filled which means it will always be same stactrace regardless of place from which it was thrown. If stacktrace is not important for you than you could try your idea but this could make debugging very hard if not impossible in some cases.
@Pshemo I don't plan to actually do this in code, I'm asking about the performance, and using this absurdity as an example where it could make a difference.
@MartinCarney I've added an answer to your last paragraph ie would caching an Exception have a performance gain. If it's useful I can add the code, if not I can delete the answer.

M
Michael

Creating an exception object is not necessarily more expensive than creating other regular objects. The main cost is hidden in native fillInStackTrace method which walks through the call stack and collects all required information to build a stack trace: classes, method names, line numbers etc.

Most of Throwable constructors implicitly call fillInStackTrace. This is where the idea that creating exceptions is slow comes from. However, there is one constructor to create a Throwable without a stack trace. It allows you to make throwables that are very fast to instantiate. Another way to create lightweight exceptions is to override fillInStackTrace.

Now what about throwing an exception? In fact, it depends on where a thrown exception is caught.

If it is caught in the same method (or, more precisely, in the same context, since the context can include several methods due to inlining), then throw is as fast and simple as goto (of course, after JIT compilation).

However if a catch block is somewhere deeper in the stack, then JVM needs to unwind the stack frames, and this can take significantly longer. It takes even longer, if there are synchronized blocks or methods involved, because unwinding implies releasing of monitors owned by removed stack frames.

I could confirm the above statements by proper benchmarks, but fortunately I don't need to do this, since all the aspects are already perfectly covered in the post of HotSpot's performance engineer Alexey Shipilev: The Exceptional Performance of Lil' Exception.


As noted in the article and touched on here, the upshot is that the cost of throwing/catching exceptions is highly dependent on the depth of the calls. The point here is that the statement "exceptions are expensive" is not really correct. A more correct statement is that exceptions 'can' be expensive. Honestly, I think saying only use exceptions for "truly exceptional cases" (as in the article) is too strongly worded. They are perfect for pretty much anything outside the normal return flow and it's hard to detect the performance impact of using them this way in a real application.
It might be worth it to quantify the overhead of exceptions. Even in the very worst case reported in this rather exhaustive article (throwing and catching a dynamic exception with a stacktrace that is actually queried, 1000 stack frames deep), takes 80 micro seconds. That can be significant if your system needs to process thousands of exceptions per second, but otherwise is not worth worrying about. And that's the worst case; if your stacktraces are a little saner, or you don't query them stacktrace, we can process nearly a million exceptions per second.
I emphasize this because many people, upon reading that exceptions are "expensive", never stop to ask "expensive compared to what", but assume that they are "expensive part of their program", which they very rarely are.
There is one part that is not mentioned here: the potential cost in preventing optimizations from being applied. An extreme example would be the JVM not inlining to avoid "muddling" stack traces, but I have seen (micro) benchmarks where the presence or absence of exceptions would make or break optimizations in C++ before.
@MatthieuM. Exceptions and try/catch blocks do not prevent JVM from inlining. For compiled methods real stack traces are reconstructed from virtual stack frame table stored as metadata. I can't recall a JIT optimization that is incompatible with try/catch. Try/catch structure itself does not add anything to method code, it exists only as an exception table aside from the code.
e
erickson

The first operation in most Throwable constructors is to fill in the stack trace, which is where most of the expense is.

There is, however, a protected constructor with a flag to disable the stack trace. This constructor is accessible when extending Exception as well. If you create a custom exception type, you can avoid the stack trace creation and get better performance at the expense of less information.

If you create a single exception of any type by normal means, you can re-throw it many times without the overhead of filling in the stack trace. However, its stack trace will reflect where it was constructed, not where it was thrown in a particular instance.

Current versions of Java make some attempts to optimize stack trace creation. Native code is invoked to fill in the stack trace, which records the trace in a lighter-weight, native structure. Corresponding Java StackTraceElement objects are lazily created from this record only when the getStackTrace(), printStackTrace(), or other methods that require the trace are called.

If you eliminate stack trace generation, the other main cost is unwinding the stack between the throw and the catch. The fewer intervening frames encountered before the exception is caught, the faster this will be.

Design your program so that exceptions are thrown only in truly exceptional cases, and optimizations like these are hard to justify.


H
Harry

Theres a good write up on Exceptions here.

http://shipilev.net/blog/2014/exceptional-performance/

The conclusion being that stack trace construction and stack unwinding are the expensive parts. The code below takes advantage of a feature in 1.7 where we can turn stack traces on and off. We can then use this to see what sort of costs different scenarios have

The following are timings for Object creation alone. I've added String here so you can see that without the stack being written there's almost no difference in creating a JavaException Object and a String. With stack writing turned on the difference is dramatic ie at least one order of magnitude slower.

Time to create million String objects: 41.41 (ms)
Time to create million JavaException objects with    stack: 608.89 (ms)
Time to create million JavaException objects without stack: 43.50 (ms)

The following shows how long it took to return from a throw at a particular depth a million times.

|Depth| WriteStack(ms)| !WriteStack(ms)| Diff(%)|
|   16|           1428|             243| 588 (%)|
|   15|           1763|             393| 449 (%)|
|   14|           1746|             390| 448 (%)|
|   13|           1703|             384| 443 (%)|
|   12|           1697|             391| 434 (%)|
|   11|           1707|             410| 416 (%)|
|   10|           1226|             197| 622 (%)|
|    9|           1242|             206| 603 (%)|
|    8|           1251|             207| 604 (%)|
|    7|           1213|             208| 583 (%)|
|    6|           1164|             206| 565 (%)|
|    5|           1134|             205| 553 (%)|
|    4|           1106|             203| 545 (%)|
|    3|           1043|             192| 543 (%)| 

The following is almost certainly a gross over simplification...

If we take a depth of 16 with stack writing on then object creation is taking approximately ~40% of the time, the actual stack trace accounts for the vast majority of this. ~93% of instantiating the JavaException object is due to the stack trace being taken. This means that unwinding the stack in this case is taking the other 50% of the time.

When we turn off the stack trace object creation accounts for a much smaller fraction ie 20% and stack unwinding now accounts for 80% of the time.

In both cases stack unwinding takes a large portion of the overall time.

public class JavaException extends Exception {
  JavaException(String reason, int mode) {
    super(reason, null, false, false);
  }
  JavaException(String reason) {
    super(reason);
  }

  public static void main(String[] args) {
    int iterations = 1000000;
    long create_time_with    = 0;
    long create_time_without = 0;
    long create_string = 0;
    for (int i = 0; i < iterations; i++) {
      long start = System.nanoTime();
      JavaException jex = new JavaException("testing");
      long stop  =  System.nanoTime();
      create_time_with += stop - start;

      start = System.nanoTime();
      JavaException jex2 = new JavaException("testing", 1);
      stop = System.nanoTime();
      create_time_without += stop - start;

      start = System.nanoTime();
      String str = new String("testing");
      stop = System.nanoTime();
      create_string += stop - start;

    }
    double interval_with    = ((double)create_time_with)/1000000;
    double interval_without = ((double)create_time_without)/1000000;
    double interval_string  = ((double)create_string)/1000000;

    System.out.printf("Time to create %d String objects: %.2f (ms)\n", iterations, interval_string);
    System.out.printf("Time to create %d JavaException objects with    stack: %.2f (ms)\n", iterations, interval_with);
    System.out.printf("Time to create %d JavaException objects without stack: %.2f (ms)\n", iterations, interval_without);

    JavaException jex = new JavaException("testing");
    int depth = 14;
    int i = depth;
    double[] with_stack    = new double[20];
    double[] without_stack = new double[20];

    for(; i > 0 ; --i) {
      without_stack[i] = jex.timerLoop(i, iterations, 0)/1000000;
      with_stack[i]    = jex.timerLoop(i, iterations, 1)/1000000;
    }
    i = depth;
    System.out.printf("|Depth| WriteStack(ms)| !WriteStack(ms)| Diff(%%)|\n");
    for(; i > 0 ; --i) {
      double ratio = (with_stack[i] / (double) without_stack[i]) * 100;
      System.out.printf("|%5d| %14.0f| %15.0f| %2.0f (%%)| \n", i + 2, with_stack[i] , without_stack[i], ratio);
      //System.out.printf("%d\t%.2f (ms)\n", i, ratio);
    }
  }
 private int thrower(int i, int mode) throws JavaException {
    ExArg.time_start[i] = System.nanoTime();
    if(mode == 0) { throw new JavaException("without stack", 1); }
    throw new JavaException("with stack");
  }
  private int catcher1(int i, int mode) throws JavaException{
    return this.stack_of_calls(i, mode);
  }
  private long timerLoop(int depth, int iterations, int mode) {
    for (int i = 0; i < iterations; i++) {
      try {
        this.catcher1(depth, mode);
      } catch (JavaException e) {
        ExArg.time_accum[depth] += (System.nanoTime() - ExArg.time_start[depth]);
      }
    }
    //long stop = System.nanoTime();
    return ExArg.time_accum[depth];
  }

  private int bad_method14(int i, int mode) throws JavaException  {
    if(i > 0) { this.thrower(i, mode); }
    return i;
  }
  private int bad_method13(int i, int mode) throws JavaException  {
    if(i == 13) { this.thrower(i, mode); }
    return bad_method14(i,mode);
  }
  private int bad_method12(int i, int mode) throws JavaException{
    if(i == 12) { this.thrower(i, mode); }
    return bad_method13(i,mode);
  }
  private int bad_method11(int i, int mode) throws JavaException{
    if(i == 11) { this.thrower(i, mode); }
    return bad_method12(i,mode);
  }
  private int bad_method10(int i, int mode) throws JavaException{
    if(i == 10) { this.thrower(i, mode); }
    return bad_method11(i,mode);
  }
  private int bad_method9(int i, int mode) throws JavaException{
    if(i == 9) { this.thrower(i, mode); }
    return bad_method10(i,mode);
  }
  private int bad_method8(int i, int mode) throws JavaException{
    if(i == 8) { this.thrower(i, mode); }
    return bad_method9(i,mode);
  }
  private int bad_method7(int i, int mode) throws JavaException{
    if(i == 7) { this.thrower(i, mode); }
    return bad_method8(i,mode);
  }
  private int bad_method6(int i, int mode) throws JavaException{
    if(i == 6) { this.thrower(i, mode); }
    return bad_method7(i,mode);
  }
  private int bad_method5(int i, int mode) throws JavaException{
    if(i == 5) { this.thrower(i, mode); }
    return bad_method6(i,mode);
  }
  private int bad_method4(int i, int mode) throws JavaException{
    if(i == 4) { this.thrower(i, mode); }
    return bad_method5(i,mode);
  }
  protected int bad_method3(int i, int mode) throws JavaException{
    if(i == 3) { this.thrower(i, mode); }
    return bad_method4(i,mode);
  }
  private int bad_method2(int i, int mode) throws JavaException{
    if(i == 2) { this.thrower(i, mode); }
    return bad_method3(i,mode);
  }
  private int bad_method1(int i, int mode) throws JavaException{
    if(i == 1) { this.thrower(i, mode); }
    return bad_method2(i,mode);
  }
  private int stack_of_calls(int i, int mode) throws JavaException{
    if(i == 0) { this.thrower(i, mode); }
    return bad_method1(i,mode);
  }
}

class ExArg {
  public static long[] time_start;
  public static long[] time_accum;
  static {
     time_start = new long[20];
     time_accum = new long[20];
  };
}

The stack frames in this example are tiny compared to what you'd normally find.

You can peek at the bytecode using javap

javap -c -v -constants JavaException.class

ie this is for method 4...

   protected int bad_method3(int, int) throws JavaException;
flags: ACC_PROTECTED
Code:
  stack=3, locals=3, args_size=3
     0: iload_1       
     1: iconst_3      
     2: if_icmpne     12
     5: aload_0       
     6: iload_1       
     7: iload_2       
     8: invokespecial #6                  // Method thrower:(II)I
    11: pop           
    12: aload_0       
    13: iload_1       
    14: iload_2       
    15: invokespecial #17                 // Method bad_method4:(II)I
    18: ireturn       
  LineNumberTable:
    line 63: 0
    line 64: 12
  StackMapTable: number_of_entries = 1
       frame_type = 12 /* same */

Exceptions:
  throws JavaException

A
Austin

The creation of the Exception with a null stack trace takes about as much time as the throw and try-catch block together. However, filling the stack trace takes on average 5x longer.

I created the following benchmark to demonstrate the impact on performance. I added the -Djava.compiler=NONE to the Run Configuration to disable compiler optimization. To measure the impact of building the stack trace, I extended the Exception class to take advantage of the stack-free constructor:

class NoStackException extends Exception{
    public NoStackException() {
        super("",null,false,false);
    }
}

The benchmark code is as follows:

public class ExceptionBenchmark {

    private static final int NUM_TRIES = 100000;

    public static void main(String[] args) {

        long throwCatchTime = 0, newExceptionTime = 0, newObjectTime = 0, noStackExceptionTime = 0;

        for (int i = 0; i < 30; i++) {
            throwCatchTime += throwCatchLoop();
            newExceptionTime += newExceptionLoop();
            newObjectTime += newObjectLoop();
            noStackExceptionTime += newNoStackExceptionLoop();
        }

        System.out.println("throwCatchTime = " + throwCatchTime / 30);
        System.out.println("newExceptionTime = " + newExceptionTime / 30);
        System.out.println("newStringTime = " + newObjectTime / 30);
        System.out.println("noStackExceptionTime = " + noStackExceptionTime / 30);

    }

    private static long throwCatchLoop() {
        Exception ex = new Exception(); //Instantiated here
        long start = System.currentTimeMillis();
        for (int i = 0; i < NUM_TRIES; i++) {
            try {
                throw ex; //repeatedly thrown
            } catch (Exception e) {

                // do nothing
            }
        }
        long stop = System.currentTimeMillis();
        return stop - start;
    }

    private static long newExceptionLoop() {
        long start = System.currentTimeMillis();
        for (int i = 0; i < NUM_TRIES; i++) {
            Exception e = new Exception();
        }
        long stop = System.currentTimeMillis();
        return stop - start;
    }

    private static long newObjectLoop() {
        long start = System.currentTimeMillis();
        for (int i = 0; i < NUM_TRIES; i++) {
            Object o = new Object();
        }
        long stop = System.currentTimeMillis();
        return stop - start;
    }

    private static long newNoStackExceptionLoop() {
        long start = System.currentTimeMillis();
        for (int i = 0; i < NUM_TRIES; i++) {
            NoStackException e = new NoStackException();
        }
        long stop = System.currentTimeMillis();
        return stop - start;
    }

}

Output:

throwCatchTime = 19
newExceptionTime = 77
newObjectTime = 3
noStackExceptionTime = 15

This implies that creating a NoStackException is approximately as expensive as repeatedly throwing the same Exception. It also shows that creating an Exception and filling its stack trace takes approximately 4x longer.


Could you add one more case where you create one Exception instance before the start time, then throw + catch it repeatedly in a loop? That would show the cost of just throwing + catching.
@MartinCarney Great suggestion! I updated my answer to do just that.
I did some tweaking of your test code, and it looks like the compiler is doing some optimization which prevents us getting accurate numbers.
@MartinCarney I updated the answer to discount compiler optimization
FYI, you should probably read the answers to How do I write a correct micro-benchmark in Java? Hint: this isn't it.
H
Harry

This part of the question...

Another way of asking this is, if I made one instance of Exception and threw and caught it over and over, would that be significantly faster than creating a new Exception every time I throw?

Seems to be asking if creating an exception and caching it somewhere improves performance. Yes it does. It's the same as turning off the stack being written on object creation because it's already been done.

These are timings I got, please read caveat after this...

|Depth| WriteStack(ms)| !WriteStack(ms)| Diff(%)|
|   16|            193|             251| 77 (%)| 
|   15|            390|             406| 96 (%)| 
|   14|            394|             401| 98 (%)| 
|   13|            381|             385| 99 (%)| 
|   12|            387|             370| 105 (%)| 
|   11|            368|             376| 98 (%)| 
|   10|            188|             192| 98 (%)| 
|    9|            193|             195| 99 (%)| 
|    8|            200|             188| 106 (%)| 
|    7|            187|             184| 102 (%)| 
|    6|            196|             200| 98 (%)| 
|    5|            197|             193| 102 (%)| 
|    4|            198|             190| 104 (%)| 
|    3|            193|             183| 105 (%)| 

Of course the problem with this is your stack trace now points to where you instantiated the object not where it was thrown from.


C
Community

Using @AustinD's answer as a starting point, I made some tweaks. Code at the bottom.

In addition to adding the case where one Exception instance is thrown repeatedly, I also turned off compiler optimization so that we can get accurate performance results. I added -Djava.compiler=NONE to the VM arguments, as per this answer. (In eclipse, edit the Run Configuration → Arguments to set this VM argument)

The results:

new Exception + throw/catch = 643.5
new Exception only          = 510.7
throw/catch only            = 115.2
new String (benchmark)      = 669.8

So creating the exception costs about 5x as much as throwing + catching it. Assuming the compiler doesn't optimize away much of the cost.

For comparison, here's the same test run without disabling optimization:

new Exception + throw/catch = 382.6
new Exception only          = 379.5
throw/catch only            = 0.3
new String (benchmark)      = 15.6

Code:

public class ExceptionPerformanceTest {

    private static final int NUM_TRIES = 1000000;

    public static void main(String[] args) {

        double numIterations = 10;

        long exceptionPlusCatchTime = 0, excepTime = 0, strTime = 0, throwTime = 0;

        for (int i = 0; i < numIterations; i++) {
            exceptionPlusCatchTime += exceptionPlusCatchBlock();
            excepTime += createException();
            throwTime += catchBlock();
            strTime += createString();
        }

        System.out.println("new Exception + throw/catch = " + exceptionPlusCatchTime / numIterations);
        System.out.println("new Exception only          = " + excepTime / numIterations);
        System.out.println("throw/catch only            = " + throwTime / numIterations);
        System.out.println("new String (benchmark)      = " + strTime / numIterations);

    }

    private static long exceptionPlusCatchBlock() {
        long start = System.currentTimeMillis();
        for (int i = 0; i < NUM_TRIES; i++) {
            try {
                throw new Exception();
            } catch (Exception e) {
                // do nothing
            }
        }
        long stop = System.currentTimeMillis();
        return stop - start;
    }

    private static long createException() {
        long start = System.currentTimeMillis();
        for (int i = 0; i < NUM_TRIES; i++) {
            Exception e = new Exception();
        }
        long stop = System.currentTimeMillis();
        return stop - start;
    }

    private static long createString() {
        long start = System.currentTimeMillis();
        for (int i = 0; i < NUM_TRIES; i++) {
            Object o = new String("" + i);
        }
        long stop = System.currentTimeMillis();
        return stop - start;
    }

    private static long catchBlock() {
        Exception ex = new Exception(); //Instantiated here
        long start = System.currentTimeMillis();
        for (int i = 0; i < NUM_TRIES; i++) {
            try {
                throw ex; //repeatedly thrown
            } catch (Exception e) {
                // do nothing
            }
        }
        long stop = System.currentTimeMillis();
        return stop - start;
    }
}

Disabling optimization = great technique! I'll edit my original answer so as not to mislead anyone
Disabling optimization is no way better than writing a flawed benchmark, since the pure interpreted mode has nothing to do with the real-world performance. The power of JVM is JIT compiler, so what's the point of measuring something that does not reflect how real application work?
There are a lot more aspects of creating, throwing and catching exceptions than convered in this 'benchmark'. I strongly suggest you to read this post.