Reputation: 184
I'm curious about how Java does optimization for multiple "if" statements that have mutually exclusive conditions, but I don't have the knowledge to analyze it myself. The question is basically the Java version of this question Performance difference of "if if" vs "if else if"
I've seen this answered for if
statements that return
, but this question is for if
statements that have mutually exclusive conditions but don't return.
1. Multiple if statements
if (x == 0) doSomething();
if (x == 2) doSomething();
if (x == 5) doSomething();
2. Chained If-else statements
if (x == 0) doSomething();
else if (x == 2) doSomething();
else if (x == 5) doSomething();
Question
Do #1 and #2 perform the same post-compilation?
(Also: if so, how complex of a conditional can Java optimize?)
Upvotes: 12
Views: 4450
Reputation: 665
Nothing beats a good old fashioned timing test:
long total = 0;
long startTime;
long endTime;
for (int j = 0; j < 10; j++) {
startTime = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
if (i % 3 == 0) total += 1;
if (i % 3 == 1) total += 2;
if (i % 3 == 2) total += 3;
}
endTime = System.currentTimeMillis();
System.out.println("If only: " + (endTime - startTime));
startTime = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
if (i % 3 == 0) total += 1;
else if (i % 3 == 1) total += 2;
else if (i % 3 == 2) total += 3;
}
endTime = System.currentTimeMillis();
System.out.println("If-else: " + (endTime - startTime));
}
System.out.println(total);
(the 'total' value is necessary to prevent the compiler from removing the whole loop!)
The output:
If only: 215
If-else: 137
If only: 214
If-else: 121
If only: 210
If-else: 120
If only: 211
If-else: 120
If only: 211
If-else: 121
If only: 210
If-else: 121
If only: 210
If-else: 121
If only: 211
If-else: 120
If only: 211
If-else: 120
If only: 211
If-else: 121
3999999980
As we can see, the if-else blocks do run significantly faster even when the if-conditions are clearly mutually exclusive. Because the two loops take a different length of time, the compiled code must be different for each loop. Apparently the compiler doesn't optimize this. Nor does the JIT or CPU branch prediction entirely. There is still a substantial difference.
My suggestion: Use If-else whenever possible
EDIT: I also tried swapping the two loops and got the same result. If-else if much faster.
EDIT 2: I've added a for-loop around the whole test to eliminate any difference in initialization or warmup. The result is the same.
Upvotes: 4
Reputation: 10863
There is a difference, though it is rather minor. The key question is whether any step of the process is done by software smart enough to deduce that if x==0
, then x==2
and x==5
must be false.
At the Java bytecode level, they typically produce different results. There's no obligation that the compiler be smart enough to analyze the difference. (Eugene's answer to the related question shows that Sun's Java 12 compiler is indeed smart enough to make this optimization for you in some cases)
Just in time Compilers tend to be rather aggressive. They are more likely to realize that the code can only flow through one of the three branches and optimize it away. But that's still a tool-dependent statement. The Java language itself treats them as different.
Now, speaking practically, this will not matter in the slightest unless you are doing a very tight loop. The #1 rule in optimization is "profile, then optimize." There's no reason to optimize such details in at least 99% of cases.
To be specific, in the example you give, even if a compiler and JIT fails to optimize the code for you, the performance cost will be negligible. On an "average" CPU, a successfully predicted branch is roughly a tenth of the cost of a function call, so the fact that you called doSomething()
on these branches will dwarf the cost. If the extra calls cause some additional branch misprediction, you may see worse effects, but nothing as expensive as the fact that you called a function.
Now, assuming that doSomething()
was actually a placeholder for something fast like x += 1
, then you would need to profile to determine if it was right.
So, my recommendation would be to write if/if/if
or if/else if/else if
based on whichever one is correct. Whichever one makes the most sense for the kind of logic you want to use is the right answer. If this is intended to be a branch where exactly one path is taken, I'd recommend else if
. If this is intended to be a case where the function might execute many branches in the future, but it just so happens that the current list of branches are mutually exclusive, do if/if/if
to convey to the reader the intended results.
Then profile. Always profile. If you find this function is a hot spot, then consider worrying about whether the if statements are expensive.
As an aside, its difficult for a compiler to prove that they can convert if
into else if
. It must do an analysis of x to see whether it is possible for another thread to modify it. If it is a local variable, no other thread may modify it. However, if it is a member variable, its possible for another thread to modify x
in the middle of your if/if/if
block, causing it to take two paths. You may know that nobody else will modify x
like this, but the compiler has to prove it before making such an optimization, or at least prove that what it writes is consistent with its implementation for the rules for Java's Memory Model.
Upvotes: 2
Reputation: 120848
Well, only a proper JMH test would prove how fast or not a certain method is. Of course, with the caveat that you should also understand the underlying machine code if you really want to know why the numbers are the way they are. I am leaving that up to you and just presenting the numbers here in this test, only slightly showing you some specifics.
package com.so;
import java.util.concurrent.TimeUnit;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
@Warmup(iterations = 5)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Measurement(iterations = 2, time = 2, timeUnit = TimeUnit.SECONDS)
public class IfElseCompare {
public static void main(String[] args) throws Exception {
Options opt = new OptionsBuilder()
.include(IfElseCompare.class.getName())
.jvmArgs("-ea")
.build();
new Runner(opt).run();
}
private int resolveValueMultipleIfs(IfElseExecutionPlan plan) {
int x = -1;
if (plan.value() == 0) {
x = 0;
}
if (plan.value() == 1) {
x = 1;
}
if (plan.value() == 2) {
x = 2;
}
assert x != -1;
return x;
}
private int resolveValueIfElse(IfElseExecutionPlan plan) {
int x = -1;
if (plan.value() == 0) {
x = 0;
} else if (plan.value() == 1) {
x = 1;
} else if (plan.value() == 2) {
x = 2;
}
assert x != -1;
return x;
}
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@Fork(1)
public int multipleIf(IfElseExecutionPlan plan) {
return resolveValueMultipleIfs(plan);
}
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@Fork(1)
public int ifElse(IfElseExecutionPlan plan) {
return resolveValueIfElse(plan);
}
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@Fork(value = 1, jvmArgsAppend = "-Xint")
public int multipleIfsfNoJIT(IfElseExecutionPlan plan) {
return resolveValueMultipleIfs(plan);
}
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@Fork(value = 1, jvmArgsAppend = "-Xint")
public int ifElseNoJIT(IfElseExecutionPlan plan) {
return resolveValueIfElse(plan);
}
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@Fork(value = 1, jvmArgsAppend = "-XX:-TieredCompilation")
public int multipleIfsC2Only(IfElseExecutionPlan plan) {
return resolveValueMultipleIfs(plan);
}
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@Fork(value = 1, jvmArgsAppend = "-XX:-TieredCompilation")
public int ifElseC2Only(IfElseExecutionPlan plan) {
return resolveValueIfElse(plan);
}
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@Fork(value = 1, jvmArgsAppend = "-XX:TieredStopAtLevel=1")
public int multipleIfsC1Only(IfElseExecutionPlan plan) {
return resolveValueMultipleIfs(plan);
}
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@Fork(value = 1, jvmArgsAppend = "-XX:TieredStopAtLevel=1")
public int ifElseC1Only(IfElseExecutionPlan plan) {
return resolveValueIfElse(plan);
}
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@Fork(value = 1,
jvmArgsAppend = {
"-XX:+UnlockExperimentalVMOptions",
"-XX:+EagerJVMCI",
"-Dgraal.ShowConfiguration=info",
"-XX:+UseJVMCICompiler",
"-XX:+EnableJVMCI"
})
public int multipleIfsGraalVM(IfElseExecutionPlan plan) {
return resolveValueMultipleIfs(plan);
}
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@Fork(value = 1,
jvmArgsAppend = {
"-XX:+UnlockExperimentalVMOptions",
"-XX:+EagerJVMCI",
"-Dgraal.ShowConfiguration=info",
"-XX:+UseJVMCICompiler",
"-XX:+EnableJVMCI"
})
public int ifElseGraalVM(IfElseExecutionPlan plan) {
return resolveValueIfElse(plan);
}
}
And here are the results:
IfElseCompare.ifElse avgt 2 2.826 ns/op
IfElseCompare.multipleIf avgt 2 3.061 ns/op
IfElseCompare.ifElseC1Only avgt 2 3.927 ns/op
IfElseCompare.multipleIfsC1Only avgt 2 4.397 ns/op
IfElseCompare.ifElseC2Only avgt 2 2.507 ns/op
IfElseCompare.multipleIfsC2Only avgt 2 2.428 ns/op
IfElseCompare.ifElseGraalVM avgt 2 2.587 ns/op
IfElseCompare.multipleIfsGraalVM avgt 2 2.854 ns/op
IfElseCompare.ifElseNoJIT avgt 2 232.418 ns/op
IfElseCompare.multipleIfsfNoJIT avgt 2 303.371 ns/op
If you decompile the version with multiple if
conditions:
0x000000010cf8542c: test %esi,%esi
0x000000010cf8542e: je 0x000000010cf8544f ;*ifne {reexecute=0 rethrow=0 return_oop=0}
; - com.so.IfElseCompare::resolveValueMultipleIfs@3 (line 21)
0x000000010cf85430: cmp $0x1,%esi
0x000000010cf85433: je 0x000000010cf8545e ;*if_icmpne {reexecute=0 rethrow=0 return_oop=0}
; - com.so.IfElseCompare::resolveValueMultipleIfs@10 (line 25)
0x000000010cf85435: cmp $0x2,%esi
0x000000010cf85438: je 0x000000010cf8546e ;*if_icmpne {reexecute=0 rethrow=0 return_oop=0}
; - com.so.IfElseCompare::resolveValueMultipleIfs@17 (line 29)
A series of cmp/je
- compare and jump if equal, well, very much expected.
The decompiled code for if/else
is the same thing (I'll let you decompile and see with your own eyes); The generated ASM code using (java-12):
java -XX:+UnlockDiagnosticVMOptions
-XX:CICompilerCount=2
-XX:-TieredCompilation
"-XX:CompileCommand=print,com/so/IfElseCompare.resolveValueMultipleIfs"
com.so.IfElseCompare
Upvotes: 2
Reputation: 1939
Let me tell you how conditional operator "if()" works. When you write if() statement, it checks the truthfulness of the condition that you have provided in these "()". If the condition fails then the compiler looks for alternate statement or block of code which can be used when if() condition fails. Now for this alternate content we use "else" block.
Now according to your question, the answer is pretty easy to understand. There is a big difference in both ways.
1). Multiple If statements
if (x == 0) doSomething();
if (x == 2) doSomething();
if (x == 5) doSomething();
In above code all the if statements will be parsed by the compiler whether any of the condition is satisfied or not. Because they are used separately and without any alternative part.
2). Chained If-else statements
if (x == 0) doSomething();
else if (x == 2) doSomething();
else if (x == 5) doSomething();
Now in above code there is one main condition checker (x==0)
now if this fails then there are other alternatives present so compiler will check them until it finds the satisfactory solution.
Performance Issue
In first case the compiler has to check each condition since they are all separate and this may take more time. But in second case it will only compile the "else if" part when the if() statement fails to meet the conditions. So yes there can be a little bit difference between them in case of performance.
I hope it helps.
Upvotes: -2