Last time we saw that CLR exception handling is significantly slower than HotSpot exception handling. This time we'll look at two very variations of the ExceptionPerf1 microbenchmark that significantly affect performance.
I've highlighted the changes.
Variation 1
public class ExceptionPerf2 {
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
try {
Integer.parseInt("");
}
catch (NumberFormatException x) {
}
}
long end = System.currentTimeMillis();
System.out.println(end - start);
}
}
Variation 2
public class ExceptionPerf3 {
static NumberFormatException exc;
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
try {
Integer.parseInt(null);
}
catch (NumberFormatException x) {
exc = x;
}
}
long end = System.currentTimeMillis();
System.out.println(end - start);
}
}
Results:
|
HotSpot 1.6 x86 |
.NET 1.1 SP1 |
.NET 2.0 SP1 x86 |
Mono 1.9 x86 |
ExceptionPerf1 |
111 |
14743 |
3590 |
537 |
ExceptionPerf2 |
140 |
15735 |
10761 |
36309 |
ExceptionPerf3 |
112 |
14946 |
9728 |
24107 |
.NET/Mono results with IKVM 0.36
Why do these small changes have such a big perf impact?
Both these changes result in additional stack trace data being collected. IKVM has some optimizations that prevent gathering stack traces in very specific circumstances. Normally when you create a Java exception object, the Throwable
constructor will call Throwable.fillInStackTrace()
. However, since this is a very expensive operation, IKVM tries to remove this call when it is unnecessary (i.e. when it sees that you immediately throw the exception and the exception type doesn't override Throwable.fillInStackTrace()
). Additionally, in Java an exception object will always have the complete stack trace, but a .NET exception only has the stack frames from the throw to the catch site. This means that at the catch site IKVM will collect the rest of the stack trace, unless the exception object isn't used (as in the ExceptionPerf1 microbenchmark).
The time it takes to collect a stack traces obviously depends on the call stack depth, so let's look at a microbenchmark to measure that effect:
class ExceptionPerf4 {
public static void main(String[] args) {
nest(Integer.parseInt(args[0]));
}
static void nest(int depth) {
if (depth > 0) {
nest(depth - 1);
} else {
run();
}
}
static void run() {
Exception x = new Exception();
long start = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
x.fillInStackTrace();
}
long end = System.currentTimeMillis();
System.out.println(end - start);
}
}
Results:
Depth |
HotSpot 1.6 x86 |
.NET 1.1 SP1 |
.NET 2.0 SP1 x86 |
Mono 1.9 x86 |
1 |
64 |
2930 |
4611 |
19377 |
10 |
85 |
3814 |
6787 |
34895 |
100 |
380 |
12500 |
27935 |
|
1000 |
3543 |
|
|
|
For the curious, the IKVM implementation of Throwable.fillInStackTrace()
is essentially new System.Diagnostics.StackTrace(true);
Next time we'll wrap things up.