Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a performance difference between multiple "if" statements vs. "if else if" for mutually exclusive conditions?

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?)

like image 457
Jeck Avatar asked May 02 '19 22:05

Jeck


People also ask

What is the difference between multiple if statements and else if?

If statements are executed independent of one another; each one will run. Else if statements only execute if the previous if s fail.

What is better than multiple if statements?

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.

Is else if more efficient than if?

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.

Does if-else affect performance?

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.


2 Answers

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.

like image 52
Daniel Williams Avatar answered Nov 09 '22 03:11

Daniel Williams


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
like image 23
Eugene Avatar answered Nov 09 '22 03:11

Eugene