Search code examples
javaperformanceif-statementenumsswitch-statement

Why is Java's switch enum so painfully slow on the first run compared to its 'if' equivalent?


Why is Java's switch enum so painfully slow on the first run compared to its 'if' equivalent?

I'm aware that the JVM needs to "warm-up" before performance can be reliably measured. Therefore every first call is much much slower than any subsequent one. This does not mean we cannot measure the performance based on every first run.

The criteria for the test are:

  1. Always perform a fresh run.
  2. Measure time in nanoseconds to execute a single function which always returns an integer based on passed value evaluated either by if statements or a switch statement.
  3. Store returned value and print it in the end, so it doesn't get discarded in the process.

I tested enums first and expected a slight difference in performance.

Instead I got an average of:

  • 77596 nanoseconds - on if
  • 585232 nanoseconds - on switch

I wanted to see if only enums have this unfavorable property, so I also tested it with integers and strings (since Java 7 it is possible to use strings in switch statements)

INTS:

  • 2308 nanoseconds - on if
  • 1950 nanoseconds - on switch

STRINGS:

  • 8517 nanoseconds - on if
  • 8322 nanoseconds - on switch

Both these tests yield very similar results, suggesting that if and switch statements are equivalent, very similar or equally good on every run, however this is not the case with enums.

I tested this both on Windows and Linux with both Java 8 and Java 17.

Here is the switch enum code:

public class SwitchEnum{
    public static void main(String[] args){
        long st = System.nanoTime();
        int val = getValue(Day.FRIDAY);
        long en = System.nanoTime();
        System.out.println("SwitchEnum perf nano: " + (en - st));
        System.out.println("Sum: " + val);
    }

    public static int getValue(Day day){
        switch (day){
            case MONDAY:
                return 7;
            case TUESDAY:
                return 3;
            case WEDNESDAY:
                return 5;
            case THURSDAY:
                return 2;
            case FRIDAY:
                return 1;
            case SATURDAY:
                return 6;
            case SUNDAY:
                return 4;
            default:
                throw new RuntimeException();
        }
    }
}

Here is the if enum code:

public class IfEnum{
    public static void main(String[] args){
        long st = System.nanoTime();
        int val = getValue(Day.FRIDAY);
        long en = System.nanoTime();
        System.out.println("IfEnum perf nano: " + (en - st));
        System.out.println("Sum: " + val);
    }

    public static int getValue(Day day){
        if (day == Day.MONDAY){
            return 7;
        }else if (day == Day.TUESDAY){
            return 3;
        }else if (day == Day.WEDNESDAY){
            return 5;
        }else if (day == Day.THURSDAY){
            return 2;
        }else if (day == Day.FRIDAY){
            return 1;
        }else if (day == Day.SATURDAY){
            return 6;
        }else if (day == Day.SUNDAY){
            return 4;
        }else{
            throw new RuntimeException();
        }
    }
}

And the enum:

public enum Day{
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;
}

I also tested this in C and C# to see if switch statements on enums have a significant performance drawback compared to its if equivalents - there was none. I've also noticed that if we provide an instruction in 'default' or equivalent 'else' the performance also increases so I included it in all tests.

This question is not about the typical "if vs switch" battle, but rather what's going on with enums and switch statements.

In any case why should the switch with enums be on average 7 times slower than it's equivalent? What could be the cause of this?

It seems like I have been misunderstood. In truth the original enum was completely different, as I was trying to find the culprit of the 'unreasonable overhead' I came up with this benchmark.

Funnily enough, warming up the JVM doesn't help the performance of that function at all.

You can put some nested loops before the method in question:

public static void main(String[] args) throws InterruptedException{
        for (int i = 0; i < 1000; i++){
            for (int j = 0; j < 1000; j++){
                System.out.println(j);
            }
            System.out.println(i);
        }
        Thread.sleep(100);
        for (int i = 0; i < 1000; i++){
            System.out.println(i);
        }
        long st = System.nanoTime();
        int val = getValue(Day.FRIDAY);
        long en = System.nanoTime();
        System.out.println("SwitchEnum perf nano: " + (en - st));
        System.out.println("Sum: " + val);
    }

The only thing that matters is if it was already called. Every subsequent call is optimized. Whether it's a constructor, function or an object's method. The fact is that if you're initializing a framework you will only call the 'initialize()' method once (which will in turn call other methods on its way). In this particular case the only thing you'd care about is the performance of the first invocation of a function. Let's suppose that your framework calls 8000 methods when it's first launched. Each method takes 1ms to execute, so it propagates to 8 seconds on every run. And the Java community is simply going to say "you're benchmarking it incorrectly"? No. This is how long it takes to get that particular framework up and running. Naturally performance is lost here and there. You can always make it faster and better. There is no reason for the switch enum statement to add 0.6ms to the clock given that its 'if' equivalent takes 0.1ms.

So here I am asking, what is the source of this overhead?


Solution

  • When you have a symbolic reference like case FRIDAY:, you expect it to be executed for the enum constant FRIDAY, regardless of the other constants in that class or, in other words, to keep working even if constants were added or removed before it or the declaration order changed.

    This is mandated by the specification:

    Adding or reordering enum constants in an enum class will not break compatibility with pre-existing binaries.

    The specification does not tell how compilers should achieve this and there are interesting differences between the existing compilers.

    Both compilers, javac and ecj, will generate code for creating an int[] array, mapping the enum’s runtime ordinal numbers to the numbers used in the compiled bytecode (as bytecode instructions only switch over int values). Since both compilers generate the code in a way that the array is created the first time, the switch statement is executed, and then reused, this is a common cause for a higher first-time execution overhead.

    When you look at the javac compiled code, you’ll notice another class file, like SwitchEnum$1.class, alongside your class. This class will hold the mentioned array, created in the class initializer. This follows a known pattern for having a thread safe lazy initialization without the need for synchronization primitives for subsequent accesses. But of course, it adds to the first time initialization overhead. In the worst case, depending on the JVM, you have the costs of loading, verifying, and initializing the second class right at the first execution of the switch statement.

    Eclipse, on the other hand, adds a private static volatile field to the class containing the switch statement and will initialize it lazily based on a null test. So there is not the initialization overhead of a second class, but a volatile write, which might be faster in the first execution. But this code will pay the price of a volatile read every time, the switch statement is subsequently executed. Though, we should be thankful for the volatile read, as for more than a decade, eclipse just produced code that was not thread safe, with low chances of identifying the problem if you ever stumbled over this, as there’s no mutable state visible in the source code.

    Even after the initialization, there can be a performance penalty due to the fact that the code depends on the contents of an int[] array created at runtime, hence, is less predictable than the sequence of if statements. This depends on the surrounding code and usually doesn’t matter. But the JDK developers stumbled across this issue at least once themselves and created this ticket. The report contains a link to the scenario where it happened to be truly performance relevant.

    There’s a discussion to use invokedynamic for switch in the future, which would not change the first time overhead, but improve the subsequent executions, as after linking, the code will be entirely predictable for the JIT, especially for the 99% of all cases where the mapping would be an identity function.

    One improvement has been made to JDK 21’s javac: it will not use this array indirection when the switch statement is within the enum type itself, as in this case, the constants can’t get reordered without recompiling the switch statement. This is also fixing this bug.

    Note that the array is shared between all switch statements over the same enum type within one class, so even if one statement is executed only once, other statements may still benefit from the already initialized array. If there truly is only one switch statement in this class, executed at most once, e.g. in a class initializer, and initialization time matters that much, or you truly experience a performance bottleneck at one specific switch statement, you may resort to Elliott Frisch’s answer and put the intended value into the enum type itself (if you have the option to change the enum type). Otherwise, you may use if statements at that specific place.

    For code executed more than once, an EnumMap may also help if you can’t modify the enum type itself.