|
The Java Specialists' Newsletter
Issue 187 2010-08-31
Category:
Performance
Java version: Java 6 Cost of Causing Exceptionsby Dr. Heinz M. KabutzAbstract: Many years ago, when hordes of C++ programmers ventured to the greener pastures of Java, some strange myths were introduced into the language. It was said that a "try" was cheaper than an "if" - when nothing went wrong.
Welcome to the 187th issue of The Java(tm) Specialists' Newsletter. Summer is rapidly
ending here in Crete. We have not had a drop of rain since
June, but the temperature is now a chilly 28 degrees Celsius.
I will need to soon check that my central heating system is
operational. Our kids are still on holiday for a couple of
weeks, having broken up for their summer break in the middle
of June. If I ever had a chance be a child again, I would
choose Greece as the place to grow up. Sun, sea and every
three years on holiday for one (on average, not counting
strikes by teachers or pupils).
I spent my afternoon reading through our club forum. I wanted to see where I
could add to the discussions. What I found interesting was
how civil and polite all the posts were. Not a single
emotional outburst or personal attack. The questions were
also of a high technical standard. It is great to see that
the club is becoming a valuable resource for our
members. We have now added an annual payment option for those
who would like to sign up. It works out at $49.95 per month,
rather than $69.95. (I had considered offering the annual
payment option when I first started, but was not sure whether
the club would ever take off. Now that it is firmly
established, I am happy to give a yearly payment option with
a nice discount.)
Would you like to really understand Java concurrency? Join us for an
in-depth study of how threading works in Java. During the course,
you will learn how to write correct and fast multi-threaded Java code.
Please
click here if you would like to learn more. Cost of Causing Exceptions
A few weeks ago, Herman Lintvelt
sent me a discussion he had with some of the Java programmers
he was mentoring. One of the programmers had read that a
try/catch was for free and so he was doing things like:
try {
return policy.calculateInterest();
} catch(NullPointerException ex) {
// policy object was null
return 0.0f;
}
instead of using an if-else construct:
if (policy != null) {
return policy.calculateInterest();
} else {
// policy object was null
return 0.0f;
}
This led to Java code that was hard to understand. However,
it was equally difficult to persuade Herman's prodigy that we
should not write such code. After all, he kept pointing out
that try was faster than
if.
In this newsletter, we will try to persuade you that it is a
bad coding practice to cause exceptions unnecessarily. We
will then explain why these two approaches take almost the
same amount of time.
Causing Exceptions: Style
Here are some reasons why it is better to check conditions
rather than cause exceptions:
1. Exceptions should be used to indicate an exceptional or error
condition. We should not use them to change the control flow
of our program. When we use them to avoid an if-else for
null, we are effectively using them to steer the control flow
of our program. This makes it difficult to understand the
code.
2. It is not at all clear that the NullPointerException is
happening due to policy being null. What if the
method calculateInterest() causes a
NullPointerException internally due to a programming bug?
All we would see is that we calculate zero interest, hiding a
potentially serious error.
3. Debuggers are often set up to pause whenever an exception
is generated. By causing exceptions in our code, we make
debugging of our code almost impossible.
This list is by no means complete. There are many other
reasons why we should not code like this. In fact, it would
be better to use the Null Object Pattern instead of testing
for null, that way the behaviour for when policy
is null is well defined. Something like:
return policy.calculateInterest();
Where policy might be an instance of NullPolicy:
public class NullPolicy implements Policy {
public float calculateInterest() {
return 0.0f;
}
}
Performance
The first time I saw this strange construct was in 1999,
whilst mentoring C++ programmers on this new wonder called
"Java". In order to make Java run faster, they wrote
equals() methods that assumed everything would be fine,
returning false when a
RuntimeException occurred.
For many years, it was faster to just do a try, rather than
an if statement (unless of course an exception occurred, in
which case it was much much slower). Even though the code
was ugly, it did perform a tiny little bit faster.
Constructing exceptions is very expensive, we know that.
For most objects, the cost of construction in terms of time
complexity is constant. With exceptions, it is related to
the call stack depth. Thus it is more expensive to generate
exceptions deep down in the hierarchy.
However, a few months ago, I tried to demonstrate to some
Java Master students how costly it really was to construct
exceptions. For some reason, my test did not work. It in
fact demonstrated that it is extremely fast to cause
exceptions. We had run out of time for that day, so I was
unable to investigate the exact reasons for our weird
results.
Herman also tried to write some code that clearly
demonstrated that it was much slower to occasionally cause
exceptions, rather than use an if-else statement all the
time. In his code, he had however used Math.random() to
decide whether an object would be null or not. The call to
Math.random() swamped the rest of his results, so that he
could not get clear results.
I rewrote his test slightly by creating an array of
random objects and then calling the methods on the objects.
According to some factor, I would have a certain percentage
of null values in the array, which
would occasionally cause NullPointerException. I expected
the cost of creating the exceptions to be much greater than
the cost of the if statements. However, it turns out that
the speed for both was roughly the same.
After causing the same NullPointerException a number of
times, Java starts returning the same exception instance to
us. Here is a piece of code that demonstrates this nicely:
import java.util.*;
public class DuplicateExceptionChecker {
private final IdentityHashMap<Exception, Boolean> previous =
new IdentityHashMap<Exception, Boolean>();
public void handleException(Exception e) {
checkForDuplicates(e);
}
private void checkForDuplicates(Exception e) {
Boolean hadPrevious = previous.get(e);
if (hadPrevious == null) {
previous.put(e, false);
} else if (!hadPrevious) {
notifyOfDuplicate(e);
previous.put(e, true);
}
}
public void notifyOfDuplicate(Exception e) {
System.err.println("Duplicate Exception: " + e.getClass());
System.err.println("count = " + count(e));
e.printStackTrace();
}
private int count(Exception e) {
int count = 0;
Class exceptionType = e.getClass();
for (Exception exception : previous.keySet()) {
if (exception.getClass() == exceptionType) {
count++;
}
}
return count;
}
}
We would expect to eventually run out of memory, as obviously
all new exceptions would be brand new objects? I was
surprised to discover that this was not true for the server
HotSpot compiler. After a relatively short while, it began
returning the same exception instance, with an empty stack
trace. You might have seen empty stack traces in your
logs. This is why they occur. Too many exceptions are
happening too closely together and eventually the server
HotSpot compiler optimizes the code to deliver a single
instance back to the user.
Here is a class that uses the DuplicateExceptionChecker to
check for NullPointerException duplicates. What we do is
occasionally set the array elemenet to null, which causes
the NullPointerException when we call
randomObjects[j].toString().
import java.util.*;
public class NullPointerTest extends DuplicateExceptionChecker {
private final Object[] randomObjects =
new Object[1000 * 1000];
private final String[] randomStrings =
new String[1000 * 1000];
public static void main(String[] args) {
NullPointerTest npt = new NullPointerTest();
npt.fillArrays(0.01);
npt.test();
}
public void notifyOfDuplicate(Exception e) {
super.notifyOfDuplicate(e);
System.exit(1);
}
private void fillArrays(double probabilityObjectIsNull) {
Random random = new Random(0);
for (int i = 0; i < randomObjects.length; i++) {
if (random.nextDouble() < probabilityObjectIsNull) {
randomObjects[i] = null;
} else {
randomObjects[i] = new Integer(i);
}
}
Arrays.fill(randomStrings, null);
}
private void test() {
for (int i = 0; i < 100; i++) {
for (int j = 0; j < randomObjects.length; j++) {
try {
randomStrings[j] = randomObjects[j].toString();
} catch (NullPointerException e) {
randomStrings[j] = null;
handleException(e);
}
}
}
}
}
After a short while, I see output such as:
Duplicate Exception: class java.lang.NullPointerException
count = 228
java.lang.NullPointerException
This is true also for other exceptions that you could cause
with the JVM. For example, the ClassCastException:
import java.util.*;
public class ClassCastTest extends DuplicateExceptionChecker {
private final Object[] randomObjects =
new Object[1000 * 1000];
private final String[] randomStrings =
new String[1000 * 1000];
public static void main(String[] args) {
ClassCastTest npt = new ClassCastTest();
npt.fillArrays(0.01);
npt.test();
}
public void notifyOfDuplicate(Exception e) {
super.notifyOfDuplicate(e);
System.exit(1);
}
private void fillArrays(double probabilityObjectIsNull) {
Random random = new Random(0);
for (int i = 0; i < randomObjects.length; i++) {
if (random.nextDouble() < probabilityObjectIsNull) {
randomObjects[i] = new Float(i);
} else {
randomObjects[i] = new Integer(i);
}
}
Arrays.fill(randomStrings, null);
}
private void test() {
for (int i = 0; i < 100; i++) {
for (int j = 0; j < randomObjects.length; j++) {
try {
randomStrings[j] = ((Integer)randomObjects[j]).toString();
} catch (ClassCastException e) {
randomStrings[j] = null;
handleException(e);
}
}
}
}
}
We can see that this also causes an exception that eventually
is replaced with a single instance exception.
Duplicate Exception: class java.lang.ClassCastException
count = 263
java.lang.ClassCastException
Lastly we can see that even an ArrayIndexOutOfBoundsException
is eventually replaced with a single instance without a
stack trace:
import java.util.*;
public class ArrayBoundsTest extends DuplicateExceptionChecker {
private static final Object[] randomObjects =
new Object[1000 * 1000];
private static final int[] randomIndexes =
new int[1000 * 1000];
private static final String[] randomStrings =
new String[1000 * 1000];
public static void main(String[] args) {
ArrayBoundsTest test = new ArrayBoundsTest();
test.fillArrays(0.01);
test.test();
}
public void notifyOfDuplicate(Exception e) {
super.notifyOfDuplicate(e);
System.exit(1);
}
private void fillArrays(double probabilityIndexIsOut) {
Random random = new Random(0);
for (int i = 0; i < randomObjects.length; i++) {
randomObjects[i] = new Integer(i);
randomIndexes[i] = (int) (Math.random() * i);
if (random.nextDouble() < probabilityIndexIsOut) {
randomIndexes[i] = -randomIndexes[i];
}
}
Arrays.fill(randomStrings, null);
}
private void test() {
for (int i = 0; i < 100; i++) {
for (int j = 0; j < randomObjects.length; j++) {
try {
int index = randomIndexes[j];
randomStrings[index] = randomObjects[index].toString();
} catch (ArrayIndexOutOfBoundsException e) {
randomStrings[j] = null;
handleException(e);
}
}
}
}
}
The question still remains - what is faster? I will not
answer that question in this newsletter. Since we do not
have the cost of object creation during the exception, the
number of instructions would be roughly the same.
There is thus a difference between causing and
creating exceptions. When we create them ourselves,
they are very expensive to initialize due to the
fillInStackTrace() method. But when we cause
them inside the Java Virtual Machine, they may end up eventually
not costing anything, depending on the exception caused. This is an
optimization that we could of course add to our own code, but
just remember that the stack trace is one of the most
important parts of the exception. Leave that out and you
might as well not throw it.
History Lesson
When I discovered this new feature, I was amazed as to how
clever the Sun engineers had been to sneak this into the very
latest Java 6 server HotSpot. Wanting to find out at exactly
which point this was added, I went back in time and tried
earlier versions of Java 6, then Java 5, 1.4.2, 1.4.1 and
1.4.0. Imagine my surprise when every single version that I
tried had this feature? It was actually a bit depressing
that the JVM has been doing this since at least February 2002
and that I did not know about it. I have also never read
about this anywhere.
Turning Off Fast Exceptions
Ervin Varga sent
me a JVM option that you can use to control this behaviour
in the JVM, if you ever need to. Use the
-XX:-OmitStackTraceInFastThrow to turn it off.
Thank you very much for reading this newsletter. I hope you
enjoyed it.
Heinz
Performance Articles
Related Java Course
Discuss at The Java Specialist Club
|