Search code examples
javaperformancejit

Performance of behaviorally identical conditional checks


I answered this question and noticed something that intrigued me while running the code for it. Part of the question was about the performance of various styles of identical conditional checks, for example:

if (x > 0) { 
    if (y > 0) { 
        if (z > 0) { 
            if (complexCondition()) { 
                 noOp();
            }
        }
    }
}

versus

if (x > 0 && y > 0 && z > 0 && complexCondition()) { 
     noOp();
}

I wrote a program that times how long a certain number of iterations takes to execute, and the results are what I don't understand.

Here's the output of my program on my machine (Win 8.1 64-bit, javac 1.8.0_11 (Oracle), java 1.0.0_11 (Java SE, Oracle). I've ran each trial multiple times and they yield repeatable results.

x = 9, y = 2, z = 8, countTrials = 1, iterationsPerTrial = 2736000
Method        Average Runtime(ns)
guardIf()     5746173
multipleIf()  4868180
chainedIf()   9316172

x = 9, y = 2, z = 8, countTrials = 100, iterationsPerTrial = 2736000
guardIf()     1642750
multipleIf()  1121897
chainedIf()   1739522

x = -1, y = 2, z = 8, countTrials = 1, iterationsPerTrial = 2736000 // I expect shorter results with x = -1, because they all short curcuit
guardIf()     9245313
multipleIf()  8728608
chainedIf()   11718332

x = -1, y = 2, z = 8, countTrials = 100, iterationsPerTrial = 2736000 
guardIf()     1754279
multipleIf()  1611278
chainedIf()   4947295

Millisecond measurements (implemented with org.apache.commons.lang3.time.StopWatch)

x = 9, y = 2, z = 8, countTrials = 1, iterationsPerTrial = Integer.MAX_VALUE
Method        Average Runtime(ms)
guardIf()     1664
multipleIf()  1095
chainedIf()   1654

x = -1, y = 2, z = 8, countTrials = 1, iterationsPerTrial = Integer.MAX_VALUE
Method        Average Runtime(ms)
guardIf()     4886
multipleIf()  4926
chainedIf()   4862

x = 9, y = 2, z = 8, countTrials = 10, iterationsPerTrial = Integer.MAX_VALUE
Method        Average Runtime(ms)
guardIf()     1673
multipleIf()  1108
chainedIf()   1682

x = -1, y = 2, z = 8, countTrials = 10, iterationsPerTrial = Integer.MAX_VALUE
Method        Average Runtime(ms)
guardIf()     4364
multipleIf()  4363
chainedIf()   4877

Why do runs of all methods takes longer (in the case of chainIf, almost 300% more time) when the values they are checking allow them all to short curcuit?

I've stepped through them all with the debugger, and they do infact short curcuit (as I'd expect). I've examined the bytecode and chainedIf() and multipleIf() are exactly identical. I'm stumped and curious.

I'm not sure if there's some flaw in the way I'm performing my measurement, so I've included my program below.

Program source

class TrialResult {
    public long GuardIf = 0; 
    public long MultipleIf = 0; 
    public long ChainedIf = 0; 

    public TrialResult(long guardIf, long multipleIf, long chainedIf) { 
        this.GuardIf = guardIf; 
        this.MultipleIf = multipleIf; 
        this.ChainedIf = chainedIf; 
    }
}

public class Program {

    private int x; 
    private int y; 
    private int z; 

    public static void main(String[] args) { 
        Program program = new Program(); 
        List<TrialResult> trials = new ArrayList<TrialResult>(); 
        int countTrials = 1; 

        for (int j = 0; j < countTrials; j++) {     
            long t0 = 0, t1 = 0; 

            t0 = System.nanoTime();
            for (long i = 0; i < 2073600; i++) { 
                program.chainedIf();
            }
            t1 = System.nanoTime();
            long chainIf = t1 - t0;

            t0 = System.nanoTime();
            for (long i = 0; i < 2073600; i++) { 
                program.multipleIf();
            }

            t1 = System.nanoTime();
            long multipleIf = t1 - t0;

            t0 = System.nanoTime();
            for (long i = 0; i < 2073600; i++) { 
                program.guardIf();
            }
            t1 = System.nanoTime();
            long guardIf = t1 - t0;
            System.out.printf("Trial %d completed\r\n", j+1);

            trials.add(new TrialResult(guardIf, multipleIf, chainIf)); 
        }

        long chainIf = 0, multipleIf = 0, guardIf = 0; 
        for (TrialResult r : trials) { 
            chainIf += r.ChainedIf; 
            multipleIf += r.MultipleIf; 
            guardIf += r.GuardIf; 
        }
        System.out.printf("%d, %d, %d", guardIf / trials.size(), multipleIf / trials.size(), chainIf / trials.size()); 
    }

    private Program() {
        x = 9; 
        y = 2; 
        z = 8; 
    }

    private void chainedIf() { 
        if (x > 0) { 
            if (y > 0) { 
                if (z > 0) { 
                    if (complexCondition()) { 
                         noOp();
                    }
                }
            }
        }
    }

    private void multipleIf() { 
        if (x > 0 && y > 0 && z > 0 && complexCondition()) { 
             noOp();
        }
    }

    public void guardIf() { 
        if (x <= -1) { 
            return; 
        }

        if (y <= -1) { 
            return; 
        }

        if (z <= -1) { 
            return; 
        }

        if (!complexCondition()) { 
            return; 
        }

         noOp();
    }

    private boolean complexCondition() { 
        return (x > 0 && 
            y < x && 
            y + z > x
        );      
    }

    private void noOp() { 
        return; 
    }
}

Solution

  • Why do runs of all methods takes longer (in the case of chainIf, almost 300% more time) when the values they are checking allow them all to short curcuit?

    Because your benchmark is faulty. Please read the answer to a similar question:
    Java loop gets slower after some runs / JIT's fault?