Possible Duplicate:
How slow are Java exceptions?
The following two programs take approximately the same amount of time to run:
public class Break {
public static void main(String[] argv){
long r = 0, s = 0, t = 0;
for(long x = 10000000; x > 0; x--){
long y = x;
while(y != 1){
if(y == 0)
throw new AssertionError();
try2: {
try1: {
for(;;){
r++;
if(y%2 == 0)
break try1;
y = y*3 + 1;
}
}/*catch(Thr _1)*/{
for(;;){
s++;
if(y%2 == 1)
break try2;
y = y/2;
}
}
}/*catch(Thr _2)*/{
t++;
}
}
}
System.out.println(r + ", " + s + ", " + t);
}
}
public class Try {
private static class Thr extends Throwable {}
private static final Thr thrown = new Thr();
public static void main(String[] argv){
long r = 0, s = 0, t = 0;
for(long x = 10000000; x > 0; x--){
long y = x;
while(y != 1){
try{
if(y == 0)
throw new AssertionError();
try{
for(;;){
r++;
if(y%2 == 0)
throw thrown;
y = y*3 + 1;
}
}catch(Thr _1){
for(;;){
s++;
if(y%2 == 1)
throw thrown;
y = y/2;
}
}
}catch(Thr _2){
t++;
}
}
}
System.out.println(r + ", " + s + ", " + t);
}
}
$ for x in Break Try; do echo $x; time java $x; done Break 1035892632, 1557724831, 520446316 real 0m10.733s user 0m10.719s sys 0m0.016s Try 1035892632, 1557724831, 520446316 real 0m11.218s user 0m11.204s sys 0m0.017s
But the time taken by the next two programs is relatively different:
public class Return {
private static int tc = 0;
public static long find(long value, long target, int depth){
if(depth > 100)
return -1;
if(value%100 == target%100){
tc++;
return depth;
}
long r = find(target, value*29 + 4221673238171300827l, depth + 1);
return r != -1? r : find(target, value*27 + 4494772161415826936l, depth + 1);
}
public static void main(String[] argv){
long s = 0;
for(int x = 0; x < 1000000; x++){
long r = find(0, x, 0);
if(r != -1)
s += r;
}
System.out.println(s + ", " + tc);
}
}
public class Throw {
public static class Found extends Throwable {
// note the static!
public static int value = 0;
}
private static final Found found = new Found();
private static int tc = 0;
public static void find(long value, long target, int depth) throws Found {
if(depth > 100)
return;
if(value%100 == target%100){
Found.value = depth;
tc++;
throw found;
}
find(target, value*29 + 4221673238171300827l, depth + 1);
find(target, value*27 + 4494772161415826936l, depth + 1);
}
public static void main(String[] argv){
long s = 0;
for(int x = 0; x < 1000000; x++)
try{
find(0, x, 0);
}catch(Found _){
s += found.value;
}
System.out.println(s + ", " + tc);
}
}
$ for x in Return Throw; do echo $x; time java $x; done Return 84227391, 1000000 real 0m2.437s user 0m2.429s sys 0m0.017s Throw 84227391, 1000000 real 0m9.251s user 0m9.215s sys 0m0.014s
I'd imagine a simple try/throw/catch mechanism would look sort of like an at least-partially-tail-call-optimised return (so it is directly known where control should return to (the closest catch)), but, of course, the JRE implementation does a lot of optimisation.
Why is there a large difference in the latter but not the former? Will it be because control flow analysis determines the former two programs to be pretty much the same and actual try/throw/catches are particularly slow, or because Return's find
is unfolded to some level into something that avoids method calls while Throw's one can't be, or ..?
Thanks.
Edit: this question seems different to me to How slow are Java exceptions? because it doesn't ask why there is such a large difference in a case similar to this one. It also disregards the fact that time is spent creating the exception object (which—unless fillInStackTrace
is overridden—includes something like traversing the stack and creating an array for it). It does, however, apparently answer part of my question: “Will it be because control flow analysis determines the former two programs to be pretty much the same”—though it seems odd that it was mentioned in the answer that does the stack trace (which would presumably dwarf any actual throw/catch operation—unless it does some complicated analysis to determine that the stack is never seen, which would make @Stephen's answer odd).
Your benchmarks don't take account of JVM warmup effects. Therefore there is considerable doubt that the results you are seeing are truely indicative of how try / break / return would perform in a real program.
(You should declare each timed test in a method, and call the methods a number of times. Then you discard the output from the first few calls ... or until the numbers stabilize ... to eliminate the once-off costs of JIT compilation, class loading and so on from the figures.)
If you really want to find out what is going on, you should get the JIT compiler to dump the native code it generates for each case.
I suspect that you will find that in the first case the JIT compiler is turning the throw / catch within the method into a simple branch instruction. In the second case, it is likely that the JIT compiler is generating more complicated code ... presumably because it doesn't recognize this as equivalent to a branch.
Why the difference? Well, there is a cost / benefit trade-offs for each optimization attempted by the JIT optimizer. Each new optimization supported by the JIT compiler has an implementation and maintenance cost. And at runtime, the compiler needs to examine the code it is compiling to see if the preconditions for the optimization are met. If they are the optimization can be performed. Otherwise, the JIT compiler has wasted time.
Now in these examples, we have an exception that is (in one case) thrown and caught in the same method, and (in the other case) propagated across a method call/return boundary. In the former example, sufficient preconditions for the optimization and the (probable) optimized code sequence are both pretty straightforward. In the latter example, the optimizer has to deal with the possibility that the methods that throw and catch the exception are in different compilation units (and hence one could be reloaded), the possibility of overriding, and so on. In addition, the generate code sequence would be significantly more complicated ... a non-standard return-from-call sequence followed by a branch.
So my theory is that the JIT compiler writers didn't think that the more complicated optimization would pay off. And given that most people don't write Java code like that, they are probably right.
find
is called the same number of time in Return and Throw? - assylias-1
in inReturn
is like a throw inThrow
—it's fairly easy to see that this result is pulled directly up tomain
by looking at the method's last two lines (the only other calls of it). - sigynsurfeitBreak
vs.Try
). As Stephen points out in the answer below, it might simply be due to a lack of optimising code that uses them (as much as other code is) because the JVM developers decided the cost of doing it might outweigh the benefits of having done it. - sigynsurfeitthrow
as is usually done when someone raises an "exception". In both tests I'm simply throwing the sameThrowable
object, partly as a sort of signal. - sigynsurfeit