[ Team LiB ] Previous Section Next Section

6.1 Exceptions

In this section, we examine the cost of exceptions and consider ways to avoid that cost. First, we look at the costs associated with try-catch blocks, which are the structures you need to handle exceptions. Then, we go on to optimizing the use of exceptions.

6.1.1 The Cost of try-catch Blocks Without an Exception

try-catch blocks generally use no extra time if no exception is thrown, although some VMs may impose a slight penalty. The following test determines whether a VM imposes any significant overhead for try-catch blocks when the catch block is not entered. The test runs the same code twice, once with the try-catch entered for every loop iteration and again with just one try-catch wrapping the loop. Because we're testing the VM and not the compiler, you must ensure that your compiler has not optimized the test away; use an old JDK version to compile it if necessary. To determine that the test has not been optimized away by the compiler, you need to compile the code, then decompile it:

package tuning.exception;
  
public class TryCatchTimeTest
{
  public static void main(String[  ] args)
  {
    int REPEAT = (args.length =  = 0) ? 10000000 : Integer.parseInt(args[0]);
    Object[  ] xyz = {new Integer(3), new Integer(10101), new Integer(67)};
    boolean res;
    long time = System.currentTimeMillis( );
    res = try_catch_in_loop(REPEAT, xyz);
    System.out.println("try catch in loop took     " + 
      (System.currentTimeMillis( ) - time));
  
    time = System.currentTimeMillis( );
    res = try_catch_not_in_loop(REPEAT, xyz);
    System.out.println("try catch not in loop took " + 
      (System.currentTimeMillis( ) - time));
  
    //Repeat the two tests several more times in this method
    //for consistency checking
    ...
  }
  
  public static boolean try_catch_not_in_loop(int repeat, Object[  ] o)
  {
    Integer i[  ] = new Integer[3];
    try {
      for (int j = repeat; j > 0; j--)
      {
        i[0] = (Integer) o[(j+1)%2];
        i[1] = (Integer) o[j%2];
        i[2] = (Integer) o[(j+2)%2];
      }
      return false;
    }
    catch (Exception e) {return true;}
  }
  
  public static boolean try_catch_in_loop(int repeat, Object[  ] o)
  {
    Integer i[  ] = new Integer[3];
    for (int j = repeat; j > 0; j--)
    {
      try {
        i[0] = (Integer) o[(j+1)%2];
        i[1] = (Integer) o[j%2];
        i[2] = (Integer) o[(j+2)%2];
      }
      catch (Exception e) {return true;}
    }
    return false;
  }
}

Running this test in various VMs results in increases in the time taken by the looped try-catch test relative to the nonlooped test for some VMs; however, the latest VMs show no penalty. See Table 6-1.

Table 6-1. Extra cost of the looped try-catch test relative to the nonlooped try-catch test

VM

1.1.8

1.2.2

1.3.1

1.3.1-server

1.4.0

1.4.0-server

1.4.0 -xInt

Increase in time

~5%

~10%

None

None

None

None

~2%

6.1.2 The Cost of try-catch Blocks with an Exception

Throwing an exception and executing the catch block has a significant overhead. This overhead seems to be due mainly to the cost of getting a snapshot of the stack when the exception is created (the snapshot allows the stack trace to be printed). The cost is large: exceptions should not be thrown as part of the normal code path of your application unless you have factored it in. Generating exceptions is one place where good design and performance go hand in hand. You should throw an exception only when the condition is truly exceptional. For example, an end-of-file condition is not an exceptional condition (all files end) unless the end-of-file occurs when more bytes are expected.[1] Generally, the performance cost of throwing an exception is equivalent to several hundred lines of simple code executions.

[1] There are exceptions to the rule. For example, in Section 7.3 in Chapter 7, the cost of one exception thrown is less than the cost of repeatedly making a test in the loop, though this is seen only if the number of loop iterations is large enough.

If your application is implemented to throw an exception during the normal flow of the program, you must not avoid the exception during performance tests. Any time costs coming from throwing exceptions must be included in performance testing, or the test results will be skewed from the actual performance of the application after deployment.

To find the cost of throwing an exception, compare two ways of testing whether an object is a member of a class: trying a cast and catching the exception if the cast fails, versus using instanceof. In the code that follows, I have highlighted the lines that run the alternative tests:

package tuning.exception;
  
public class TryCatchCostTest
{
  public static void main(String[  ] args)
  {
    Integer i = new Integer(3);
    Boolean b = new Boolean(true);
    int REPEAT = 5000000;
    int FACTOR = 1000;
    boolean res;
  
    long time = System.currentTimeMillis( );
    for (int j = REPEAT*FACTOR; j > 0 ; j--)
      res = test1(i);
    time = System.currentTimeMillis( ) - time;
    System.out.println("test1(i) took " + time);
  
    time = System.currentTimeMillis( );
    for (int j = REPEAT; j > 0 ; j--)
      res = test1(b);
    time = System.currentTimeMillis( ) - time;
    System.out.println("test1(b) took " + time);
  
    //and the same timed test for test2(i) and test2(b),
    //iterating REPEAT*FACTOR times
    ...
}
  
  public static boolean test1(Object o)
  {
    try {
      Integer i = (Integer) o;
      return false;
    }
    catch (Exception e) {return true;}
  }
  
  public static boolean test2(Object o)
  {
    if (o instanceof Integer)
      return false;
    else
      return true;
  }
}

The results of this comparison show that if test2( ) (using instanceof) takes one time unit, test1( ) with the ClassCastException thrown takes over 100 time units in JDK 1.4 (see the first line in Table 6-2). The second line in Table 6-2 shows the relative cost of throwing the exception with different parameters passed to test1( ), and also shows that throwing the exception is very costly. The two lines together show that using instanceof is fairly efficient.

Table 6-2. Extra cost of try-catch blocks when exceptions are thrown

Relative times for

1.1.8

1.2.2

1.3.1

1.3.1-server

1.4.0

1.4.0-server[2]

1.4.0-xInt

test1(b)/test2(b)

~60

~35

~90

~720

~100

N/A

~30

test1(b)/test1(i)

~160

~90

~1000

~40000

~1000

N/A

~200

test2(b)/test2(i)

1

1

~10

~100

~10

N/A

~5

[2] The 1.4 JVM JIT compiler in server mode identified that the test was effectively a repeated constant expression and collapsed the loop to one call, thus eliminating the test. The costs of using exceptions are still present in 1.4.0 server mode, but this test cannot show those costs.

For VMs not running a JIT, or using HotSpot technology, the relative times for test2( ) are different depending on the object passed. test2( ) takes one time unit when returning true but, curiously, two to ten time units when returning false. This difference for a false result indicates that the instanceof operator is faster when the instance's class correctly matches the tested class. A negative instanceof test must also check whether the instance is from a subclass or interface of the tested type before it can definitely return false. Given this, it is actually quite interesting that with a simple JIT, there is no difference in times between the two instanceof tests.

Because it is impossible to add methods to classes that are compiled (as opposed to classes you have the source for and can recompile), there are necessarily places in Java code where you have to test for the type of object. Where this type of code is unavoidable, you should use instanceof, as shown in test2( ), rather than a speculative class cast. There is no maintenance disadvantage in using instanceof, nor is the code any clearer or easier to alter by avoiding its use. I strongly advise you to avoid the use of the speculative class cast, however. It is a real performance hog and ugly as well.

6.1.3 Using Exceptions Without the Stack Trace Overhead

You may decide that you definitely require an exception to be thrown, despite the disadvantages. If the exception is thrown explicitly (i.e., using a throw statement rather than a VM-generated exception such as the ClassCastException or ArrayIndexOutOfBoundsException), you can reduce the cost by reusing an exception object rather than creating a new one. Most of the cost of throwing an exception is incurred in actually creating the new exception, which is when the stack trace is filled in. Reusing an existing exception object without resetting the stack trace avoids the exception-creation overhead. Throwing and catching an existing exception object is two orders of magnitude faster than doing the same with a newly created exception object:

public static Exception REUSABLE_EXCEPTION = new Exception( );
...
  //Much faster reusing an existing exception
  try {throw REUSABLE_EXCEPTION;}
  catch (Exception e) {...}
  
  //This next try-catch is 50 to 100 times slower than the last
  try {throw new Exception( );}
  catch (Exception e) {...}

The sole disadvantage of reusing an exception instance is that the instance does not have the correct stack trace, i.e., the stack trace held by the exception object is the one generated when the exception object was created.[3] This disadvantage can be important for some situations when the trace is important, so be careful. This technique can easily lead to maintenance problems.

[3] To get the exception object to hold the stack trace that is current when it is thrown, rather than created, you must use the fillInStackTrace( ) method. Of course, this is what causes the large overhead that you are trying to avoid.

6.1.4 Conditional Error Checking

During development, you typically write a lot of code that checks the arguments passed into various methods for validity. This kind of checking is invaluable during development and testing, but it can lead to a lot of overhead in the finished application. Therefore, you need a technique for implementing error checks that can optionally be removed during compilation. The most common way to do this is to use an if block:

public class GLOBAL_CONSTANTS {
  public static final boolean ERROR_CHECKING_ON = true;
  ...
}
  
//and code in methods of other classes includes an if block like
if (GLOBAL_CONSTANTS.ERROR_CHECKING_ON)
{
  //error check code of some sort
  ...

This technique allows you to turn off error checking by recompiling the application with the ERROR_CHECKING_ON variable set to false. Doing this recompilation actually eliminates all if blocks completely, due to a feature of the compiler (see Section 3.9.1.4 in Chapter 3). Setting the value to false without recompilation also works, but avoids only the block, not the block entry test. In this case, the if statement is still executed, but the block is not entered. This still has some performance impact: an extra test for almost every method call is significant, so it is better to recompile.[4]

[4] However, this technique cannot eliminate all types of code blocks. For example, you cannot use this technique to eliminate try-catch blocks from the code they surround. You can achieve that level of control only by using a preprocessor. My thanks to Ethan Henry for pointing this out.

    Previous Section Next Section