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?)
If statements are executed independent of one another; each one will run. Else if statements only execute if the previous if s fail.
Switch statement works better than multiple if statements when you are giving input directly without any condition checking in the statements. Switch statement works well when you want to increase the readability of the code and many alternative available.
In general, "else if" style can be faster because in the series of ifs, every condition is checked one after the other; in an "else if" chain, once one condition is matched, the rest are bypassed.
In general it will not affect the performance but can cause unexpected behaviour. In terms of Clean Code unneserry if and if-else statements have to be removed for clarity, maintainability, better testing. One case where the performance will be reduced because of unnecessary if statements is in loops.
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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With