[ Team LiB ] |
6.1 ExceptionsIn 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 Exceptiontry-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.
6.1.2 The Cost of try-catch Blocks with an ExceptionThrowing 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.
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.
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 OverheadYou 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.
6.1.4 Conditional Error CheckingDuring 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]
|