|
|
|
Contents: |
|
|
|
Related content: |
|
|
|
Subscriptions: |
|
|
| Understanding the real costs
Jack
Shirazi (mailto:jack@JavaPerformanceTuning.com?cc=&subject=Exceptions
to exceptions), Director, JavaPerformanceTuning.com Kirk
Pepperdine (mailto:kirk@JavaPerformanceTuning.com?cc=&subject=Exceptions
to exceptions), CTO, JavaPerformanceTuning.com
10 Feb 2004
Java performance enthusiasts Jack Shirazi and Kirk
Pepperdine, Director and CTO of JavaPerformanceTuning.com, follow
performance discussions all over the Internet to see what's troubling
developers. In this month's stop at the JavaRanch, they counter the
campfire stories about exceptions with a detailed look at the story
behind the story.
In our first
installment of this column, we discussed the cost of throwing
exceptions. This month, we revisit the subject from a different point of
view -- how the JVM handles thrown exceptions -- and we ponder whether
optimal exception coding should be considered a premature optimization or
a best practice.
Coding crossroads: Like this, or like
that? Performance discussion groups are full of questions
like "should I code like this, which is how everyone usually does it, or
should I code like that, for better performance?" Conventional wisdom
suggests that we should avoid early optimizations and apply best practices
until performance measurement shows the need for optimization, but the
reality is that every time we write a line of code, we are making a
decision that can affect performance.
One discussion on the JavaRanch examined two alternative methods of
assuring type safety -- one throwing an exception, one using
instanceof -- and asked the question, "Which approach is
better?" Listings 1 and 2 show the two methods. Listing 1. Using instanceof to branch
Listing 1: using instanceof to branch
for (int i = 0; i < max; i++)
{
Object obj = myVector.elementAt(i);
if (obj instanceof MySpecialClass)
{
// do this
}
}
|
Listing
2. Throwing an exception to branch
for (int i = 0; i < max; i++) {
try {
MySpecialClass myClass = (MySpecialClass)myVector.elementAt(i);
// do this
} catch (ClassCastException cce) {
continue; // for loop
}
}
|
One of the dangers when asking this type of question is that you can
lose a lot of context in trying to boil the question down to a simple
example. Not having sufficient context leads to long, often confusing
discussions, because as each respondent reads the question, they each
bring their own context to the problem. And while all of this extra
context can add value, it can often distract us from the question in the
original post. With this in mind, let's see if we can filter out some
truths from the trail of messages we found in this thread.
Characteristics of an
exception The first thing that most developers will tell you
about exceptions is that they are expensive. If you continue to probe as
to why they are expensive, the most common answer will probably be that we
need to capture the current state of the execution stack. Although this is
certainly a strong component of the cost, by listing some of the
characteristics of exceptions, we can start to see that this is only the
beginning of the story. Here are some of the characteristics of an
exception:
- Can be thrown
- Can be caught
- Can be created programmatically
- Can be created by the JVM
- Is represented as a first-class object
- Has a depth of inheritance that starts at 3
- Is composed of
String s (and
StackTraceElement s from 1.4)
- Relies on the native method,
fillInStackTrace()
The major differentiator between an exception and any other object is
that it can be thrown and caught. Let's start our investigation by
examining the course of events that is triggered when an exception is
thrown.
The cost of handling an
exception To throw an exception, the JVM issues an
athrow bytecode instruction. The athrow
instruction causes the JVM to pop the exception object off the top of the
execution stack. It then searches the current execution stack frame
looking for the first catch clause that can handle an
exception of that class, or one of its superclasses. If no catch
block is found in the current stack frame, then the current stack
frame is released and the exception is re-thrown in the context of the
next stack frame, and so on until a stack frame with a suitable
catch clause is found, or the bottom of the execution stack
is reached. Ultimately, if no appropriate catch block is
found, all of the stack frames are released, and the thread is terminated
after the ThreadGroup object has been given a chance to
handle the exception (see ThreadGroup.uncaughtException ). If
an appropriate catch block is found, the program counter is reset to the
first line of code in that block.
From this description, we can see that handling a thrown exception is
quite an expensive proposition. Take another look at the list of exception
characteristics above. Note that aside from the fact that the JVM can
"spontaneously" create an exception, all of the remaining costs are no
different than those incurred during the lifecycle of any other
first-class object.
The cost of an exception as a
first-class object Now look back at Listing
2. The exception is thrown only if the cast fails. How does the JVM
process this? A checkcast operation is issued whenever an
application is required to perform a type cast. This operation does not do
anything but check to make sure that the type of the argument on the top
of stack is what is expected. If it isn't, then it throws a
ClassCastException .
A gentler way to type-check is to use the instanceof
operator, as shown in Listing
1. Among the differences between checkcast and
instanceof is that the latter leaves a 0 or 1 on the top of
the stack to indicate failure or success.
The instanceof operator follows a very strict set of rules
to determine success or failure. The rules need to take into account
whether or not a variable reference is null , an array, an
interface, or just a class. Once the type of variable has been determined,
then the hierarchy of the qualifying operand of the other side must be
searched until either a match is found or the end of the hierarchy is
reached. In the case of an array, the type of the underlying element must
undergo the same scrutiny.
In addition to this cost, once you have applied
instanceof , you typically cast the object in the subsequent
code. Performing a subsequent cast causes the checkcast
bytecode to be executed. So, the logic used to determine if the cast will
work may be repeated (assuming the JVM has not optimized the extra check
away). Even so, because using instanceof operator does not
require us to create a new object, it is a much less expensive operation
in both memory and execution resources than creating and handling an
exception. So, the answer to the original question would appear to be
obvious. Or, is it?
A hidden risk Catching
the ClassCastException also has a hidden risk, which is that
we might capture ClassCastException s thrown from the code
that would replace the "do this" comment. If this code were to throw a
ClassCastException in the middle of some operation, we would
catch it, assume it originated from the cast to
MySpecialClass , and silently ignore it, which might leave our
application in an inconsistent state.
Dynamic tuning So far, all
we have done is estimated the one-time execution cost of each of the two
proposed coding styles. Now we need to understand the conditions under
which the code will be executing so that we can determine which coding
style should be used.
Consider the case of iterating through a collection to get an idea of
the real costs incurred. If the collection were to contain a homogenous
set of objects, and if we were to perform an instanceof on
every object pulled, the procedure would impose an unnecessary cost on the
overall runtime. On the other hand, if the collection were to contain a
heterogeneous set of objects, then we would be far better off using the
instanceof operation instead of incurring the cost of a storm
of exceptions.
This scenario gives us the final clue to the best practice: Exceptions
should be reserved for exceptional situations. Using exceptions in
exceptional circumstances is ideal for performance; using checks to avoid
throwing exceptions in non-exceptional circumstances is ideal for
performance.
The final word From
all of this we can see that following best practices (here, that
exceptions should be used for exceptional circumstances) can produce the
better performance. Sometimes we need to run through considerations like
those in this article to determine exactly what best practice is, and to
determine what are the performance considerations of that best practice
and it's alternatives. But now, instead of worrying about prematurely
optimizing code, we end up with a best coding practice which is
appropriate independently of performance, and which also provides optimal
performance -- truly the best of both worlds.
Resources
About the
authors Jack Shirazi is the Director of JavaPerformanceTuning.com and author of Java
Performance Tuning (O'Reilly). In addition to his performance tuning
focus, Jack also develops intelligent agent technology. |
Kirk Pepperdine is the Chief Technical
Officer at Java Performance Tuning.com and has been focused on
object technologies and performance tuning for the last 15 years.
Kirk is a co-author of the book ANT Developer's
Handbook.
|
|
|