Jaka jest względna różnica w wydajności instrukcji if / else w porównaniu z instrukcją switch w języku Java?

124

Martwiąc się o wydajność mojej aplikacji internetowej, zastanawiam się, która z instrukcji „if / else” lub switch jest lepsza pod względem wydajności?

Anth0
źródło
6
Czy masz jakiś powód, by sądzić, że ten sam kod bajtowy nie jest generowany dla obu konstrukcji?
Pascal Cuoq
2
@Pascal: optymalizacja może zostać przeprowadzona przy użyciu wyszukiwania tabel zamiast listy ifitp.
jldupont
18
„Przedwczesna optymalizacja jest źródłem wszelkiego zła” - Donald Knuth
missingfaktor
104
Chociaż jest to zdecydowanie przedwczesna optymalizacja, „bezmyślne trzymanie się cytatu wyjętego z kontekstu jest powodem, dla którego potrzebujemy wysokiej klasy komputera wielordzeniowego tylko po to, aby wyświetlać dziś w miarę responsywny graficzny interfejs użytkownika” - Ja.
Lawrence Dol
2
Knuth ma precyzyjny umysł. Zwróć uwagę na kwalifikator „przedwczesny”. Optymalizacja to bardzo ważna kwestia. To powiedziawszy, serwer jest związany we / wy, a wąskie gardła we / wy sieci i dysku są o rząd wielkości bardziej znaczące niż cokolwiek innego, co dzieje się na serwerze.
alphazero

Odpowiedzi:

108

To jest mikro optymalizacja i przedwczesna optymalizacja, które są złe. Raczej martw się o czytelność i łatwość utrzymania danego kodu. Jeśli jest więcej niż dwa if/elsesklejone ze sobą bloki lub jego rozmiar jest nieprzewidywalny, możesz bardzo rozważyć switchstwierdzenie.

Alternatywnie możesz też pobrać Polimorfizm . Najpierw utwórz interfejs:

public interface Action { 
    void execute(String input);
}

I zdobądź wszystkie implementacje w niektórych Map. Możesz to zrobić statycznie lub dynamicznie:

Map<String, Action> actions = new HashMap<String, Action>();

Na koniec zamień if/elselub switchna coś takiego (pozostawiając na boku trywialne sprawdzenia, takie jak nullpointers):

actions.get(name).execute(input);

To może być microslower niż if/elsealbo switch, ale kod jest co najmniej o wiele lepsze w utrzymaniu.

Jeśli mówisz o aplikacjach internetowych, możesz użyć go HttpServletRequest#getPathInfo()jako klawisza akcji (ostatecznie napisz trochę więcej kodu, aby podzielić ostatnią część pathinfo w pętli, aż zostanie znaleziona akcja). Znajdziesz tutaj podobne odpowiedzi:

Jeśli martwisz się o ogólną wydajność aplikacji internetowej Java EE, ten artykuł może Ci się również przydać. Są inne obszary, które dają znacznie większy wzrost wydajności niż tylko (mikro) optymalizacja surowego kodu Java.

BalusC
źródło
1
lub zamiast tego rozważ polimorfizm
jk.
To rzeczywiście bardziej zalecane w przypadku „nieprzewidywalnej” ilości bloków if / else.
BalusC
73
Nie od razu odrzucam wczesną optymalizację jako „złą”. Bycie zbyt agresywnym jest głupie, ale w obliczu konstruktów o porównywalnej czytelności wybór takiego, o którym wiadomo, że działa lepiej, jest właściwą decyzją.
Brian Knoblauch
8
Wersja wyszukiwania HashMap może z łatwością być 10 razy wolniejsza w porównaniu do instrukcji Tablewitsch. Nie nazwałbym tego „mikroklimatem”!
x4u
7
Interesuje mnie faktyczne poznanie wewnętrznego działania Javy w ogólnym przypadku z instrukcjami przełączania - nie interesuje mnie, czy ktoś uważa, że ​​jest to związane z nadaniem priorytetów wczesnej optymalizacji. Biorąc to pod uwagę, nie mam absolutnie żadnego pojęcia, dlaczego ta odpowiedź jest tak bardzo przychylna i dlaczego jest akceptowana ... to nic nie daje odpowiedzi na wstępne pytanie.
Searchengine
125

Całkowicie zgadzam się z opinią, że przedwczesna optymalizacja jest czymś, czego należy unikać.

Ale prawdą jest, że maszyna wirtualna Java ma specjalne kody bajtowe, których można użyć do przełączania ().

Zobacz specyfikację WM ( przełącznik wyszukiwania i przełącznik tabel )

Więc może wystąpić pewien wzrost wydajności, jeśli kod jest częścią wykresu wydajności procesora.

Waverick
źródło
60
Zastanawiam się, dlaczego ten komentarz nie jest oceniany wyżej: jest najbardziej pouczający ze wszystkich. To znaczy: wszyscy już wiemy, że przedwczesna optymalizacja jest zła i nie trzeba tego wyjaśniać po raz tysięczny.
Folkert van Heusden
5
+1 Od stackoverflow.com/a/15621602/89818 wydaje się, że wzrost wydajności jest naprawdę widoczny i powinieneś zobaczyć przewagę, jeśli używasz 18+ skrzynek.
krakaj
52

Jest bardzo mało prawdopodobne, aby przełącznik if / else lub przełącznik był źródłem problemów z wydajnością. Jeśli masz problemy z wydajnością, najpierw wykonaj analizę profilowania wydajności, aby określić, gdzie są wolne miejsca. Przedwczesna optymalizacja jest źródłem wszelkiego zła!

Niemniej jednak, można mówić o względnej wydajności przełącznika w porównaniu z if / else z optymalizacjami kompilatora Java. Po pierwsze, zauważ, że w Javie instrukcje switch działają na bardzo ograniczonej domenie - liczbach całkowitych. Zasadniczo instrukcję switch można wyświetlić w następujący sposób:

switch (<condition>) {
   case c_0: ...
   case c_1: ...
   ...
   case c_n: ...
   default: ...
}

gdzie c_0, c_1, ..., i c_Nsą liczbami, które są integralnymi cele instrukcji switch i <condition>musi rozwiązać, aby wyraz całkowitej.

  • Jeśli ten zbiór jest „gęsty” - to znaczy (max (c i ) + 1 - min (c i )) / n> α, gdzie 0 <k <α <1, gdzie kjest większy niż pewna wartość empiryczna, a można wygenerować tablicę skoków, co jest bardzo wydajne.

  • Jeśli ten zestaw nie jest bardzo gęsty, ale n> = β, binarne drzewo wyszukiwania może znaleźć cel w O (2 * log (n)), co również jest wydajne.

We wszystkich innych przypadkach instrukcja switch jest dokładnie tak samo wydajna, jak równoważna seria instrukcji if / else. Dokładne wartości α i β zależą od wielu czynników i są określane przez moduł optymalizacji kodu kompilatora.

Wreszcie, oczywiście, jeśli dziedziną <condition>nie są liczby całkowite, instrukcja switch jest całkowicie bezużyteczna.

John Feminella
źródło
+1. Istnieje duża szansa, że ​​czas spędzony na sieciowym I / O łatwo przyćmiewa ten konkretny problem.
Adam Paynter
3
Należy zauważyć, że przełączniki działają z więcej niż tylko intami. Z samouczków języka Java: „Przełącznik działa z pierwotnymi typami danych byte, short, char i int. Działa również z typami wyliczeniowymi (omówionymi w sekcji Typy wyliczeniowe), klasą String i kilkoma klasami specjalnymi, które zawijają określone typy pierwotne : Character, Byte, Short i Integer (omówione w Numbers and Strings). " Wsparcie dla String jest nowszym dodatkiem; dodano w Javie 7. docs.oracle.com/javase/tutorial/java/nutsandbolts/switch.html
atraudes
1
@jhonFeminella Czy mógłbyś porównać efekty pojęcia BIG O dla String Java7 w Swtich w porównaniu do String w if / else if ..?
Kanagavelu Sugumar
Dokładniej, javac 8 nadaje wagę 3 do złożoności
czasowej zamiast
11

Użyj przełącznika!

Nienawidzę utrzymywać blokad if-else-block! Zrób test:

public class SpeedTestSwitch
{
    private static void do1(int loop)
    {
        int temp = 0;
        for (; loop > 0; --loop)
        {
            int r = (int) (Math.random() * 10);
            switch (r)
            {
                case 0:
                    temp = 9;
                    break;
                case 1:
                    temp = 8;
                    break;
                case 2:
                    temp = 7;
                    break;
                case 3:
                    temp = 6;
                    break;
                case 4:
                    temp = 5;
                    break;
                case 5:
                    temp = 4;
                    break;
                case 6:
                    temp = 3;
                    break;
                case 7:
                    temp = 2;
                    break;
                case 8:
                    temp = 1;
                    break;
                case 9:
                    temp = 0;
                    break;
            }
        }
        System.out.println("ignore: " + temp);
    }

    private static void do2(int loop)
    {
        int temp = 0;
        for (; loop > 0; --loop)
        {
            int r = (int) (Math.random() * 10);
            if (r == 0)
                temp = 9;
            else
                if (r == 1)
                    temp = 8;
                else
                    if (r == 2)
                        temp = 7;
                    else
                        if (r == 3)
                            temp = 6;
                        else
                            if (r == 4)
                                temp = 5;
                            else
                                if (r == 5)
                                    temp = 4;
                                else
                                    if (r == 6)
                                        temp = 3;
                                    else
                                        if (r == 7)
                                            temp = 2;
                                        else
                                            if (r == 8)
                                                temp = 1;
                                            else
                                                if (r == 9)
                                                    temp = 0;
        }
        System.out.println("ignore: " + temp);
    }

    public static void main(String[] args)
    {
        long time;
        int loop = 1 * 100 * 1000 * 1000;
        System.out.println("warming up...");
        do1(loop / 100);
        do2(loop / 100);

        System.out.println("start");

        // run 1
        System.out.println("switch:");
        time = System.currentTimeMillis();
        do1(loop);
        System.out.println(" -> time needed: " + (System.currentTimeMillis() - time));

        // run 2
        System.out.println("if/else:");
        time = System.currentTimeMillis();
        do2(loop);
        System.out.println(" -> time needed: " + (System.currentTimeMillis() - time));
    }
}

Mój standardowy kod C # do testów porównawczych

Bitterblue
źródło
Czy mógłbyś (kiedyś) nieco wyjaśnić, w jaki sposób wykonałeś test porównawczy?
DerMike
Bardzo dziękuję za aktualizację. To znaczy różnią się o jeden rząd wielkości - co jest oczywiście możliwe. Czy jesteś pewien, że kompilator nie tylko zoptymalizował switches?
DerMike
@DerMike Nie pamiętam, jak uzyskałem stare wyniki. Bardzo się dzisiaj zmieniłem. Ale spróbuj sam i daj mi znać, jak to się kończy.
Bitterblue
1
kiedy uruchamiam go na moim laptopie; potrzebny czas zmiany: 3585, jeśli / jeszcze potrzebny czas: 3458 więc jeśli / inaczej jest lepiej :) lub nie gorzej
halil
1
Dominującym kosztem w teście jest generowanie liczb losowych. Zmodyfikowałem test, aby wygenerować liczbę losową przed pętlą, i użyłem wartości temp, aby przesłać z powrotem do r. Przełącznik jest wtedy prawie dwa razy szybszy niż łańcuch if-else.
boneill
8

Pamiętam, że czytałem, że w kodzie bajtowym Javy istnieją 2 rodzaje instrukcji Switch. (Myślę, że było to w 'Java Performance Tuning' Jedna z nich to bardzo szybka implementacja, która używa wartości całkowitych instrukcji switch, aby poznać przesunięcie kodu do wykonania. Wymagałoby to, aby wszystkie liczby całkowite były następujące po sobie iw dobrze zdefiniowanym zakresie , Domyślam się, że użycie wszystkich wartości Enum również należałoby do tej kategorii.

Zgadzam się jednak z wieloma innymi plakatami ... martwienie się o to może być przedwczesne, chyba że jest to bardzo gorący kod.

malaverdiere
źródło
4
+1 dla komentarza do gorącego kodu. Jeśli jest w twojej głównej pętli, to nie jest przedwczesne.
KingAndrew
Tak, javac implementuje switchkilka różnych sposobów, niektóre bardziej wydajne niż inne. Ogólnie rzecz biorąc, wydajność nie będzie gorsza niż prosta „ ifdrabinka”, ale jest wystarczająco dużo odmian (zwłaszcza w przypadku JITC), że trudno jest być o wiele bardziej precyzyjnym.
Hot Licks
8

Według Cliffa Click'a w jego wykładzie Java One z 2009 roku A Crash Course in Modern Hardware :

Obecnie wydajność jest zdominowana przez wzorce dostępu do pamięci. Dominują chybienia w pamięci podręcznej - pamięć jest nowym dyskiem. [Slajd 65]

Można uzyskać pełnię slajdy tutaj .

Cliff podaje przykład (kończąc na slajdzie 30) pokazujący, że nawet jeśli procesor wykonuje zmianę nazwy rejestru, przewidywanie gałęzi i wykonanie spekulacyjne, jest w stanie rozpocząć tylko 7 operacji w 4 cyklach zegara, zanim będzie musiał zablokować się z powodu dwóch chybień w pamięci podręcznej, które wymagają 300 cykli zegara do powrotu.

Dlatego mówi, że aby przyspieszyć program, nie powinieneś zajmować się tego rodzaju drobnymi problemami, ale większymi, takimi jak to, czy wykonujesz niepotrzebne konwersje formatu danych, takie jak konwersja „SOAP → XML → DOM → SQL →… „który” przekazuje wszystkie dane przez pamięć podręczną ”.

Jim Ferrans
źródło
4

W moim teście lepsza wydajność to ENUM> MAP> SWITCH> IF / ELSE IF w Windows7.

import java.util.HashMap;
import java.util.Map;

public class StringsInSwitch {
public static void main(String[] args) {
    String doSomething = null;


    //METHOD_1 : SWITCH
    long start = System.currentTimeMillis();
    for (int i = 0; i < 99999999; i++) {
        String input = "Hello World" + (i & 0xF);

        switch (input) {
        case "Hello World0":
            doSomething = "Hello World0";
            break;
        case "Hello World1":
            doSomething = "Hello World0";
            break;
        case "Hello World2":
            doSomething = "Hello World0";
            break;
        case "Hello World3":
            doSomething = "Hello World0";
            break;
        case "Hello World4":
            doSomething = "Hello World0";
            break;
        case "Hello World5":
            doSomething = "Hello World0";
            break;
        case "Hello World6":
            doSomething = "Hello World0";
            break;
        case "Hello World7":
            doSomething = "Hello World0";
            break;
        case "Hello World8":
            doSomething = "Hello World0";
            break;
        case "Hello World9":
            doSomething = "Hello World0";
            break;
        case "Hello World10":
            doSomething = "Hello World0";
            break;
        case "Hello World11":
            doSomething = "Hello World0";
            break;
        case "Hello World12":
            doSomething = "Hello World0";
            break;
        case "Hello World13":
            doSomething = "Hello World0";
            break;
        case "Hello World14":
            doSomething = "Hello World0";
            break;
        case "Hello World15":
            doSomething = "Hello World0";
            break;
        }
    }

    System.out.println("Time taken for String in Switch :"+ (System.currentTimeMillis() - start));




    //METHOD_2 : IF/ELSE IF
    start = System.currentTimeMillis();

    for (int i = 0; i < 99999999; i++) {
        String input = "Hello World" + (i & 0xF);

        if(input.equals("Hello World0")){
            doSomething = "Hello World0";
        } else if(input.equals("Hello World1")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World2")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World3")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World4")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World5")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World6")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World7")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World8")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World9")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World10")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World11")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World12")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World13")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World14")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World15")){
            doSomething = "Hello World0";

        }
    }
    System.out.println("Time taken for String in if/else if :"+ (System.currentTimeMillis() - start));









    //METHOD_3 : MAP
    //Create and build Map
    Map<String, ExecutableClass> map = new HashMap<String, ExecutableClass>();
    for (int i = 0; i <= 15; i++) {
        String input = "Hello World" + (i & 0xF);
        map.put(input, new ExecutableClass(){
                            public void execute(String doSomething){
                                doSomething = "Hello World0";
                            }
                        });
    }


    //Start test map
    start = System.currentTimeMillis();
    for (int i = 0; i < 99999999; i++) {
        String input = "Hello World" + (i & 0xF);
        map.get(input).execute(doSomething);
    }
    System.out.println("Time taken for String in Map :"+ (System.currentTimeMillis() - start));






    //METHOD_4 : ENUM (This doesn't use muliple string with space.)
    start = System.currentTimeMillis();
    for (int i = 0; i < 99999999; i++) {
        String input = "HW" + (i & 0xF);
        HelloWorld.valueOf(input).execute(doSomething);
    }
    System.out.println("Time taken for String in ENUM :"+ (System.currentTimeMillis() - start));


    }

}

interface ExecutableClass
{
    public void execute(String doSomething);
}



// Enum version
enum HelloWorld {
    HW0("Hello World0"), HW1("Hello World1"), HW2("Hello World2"), HW3(
            "Hello World3"), HW4("Hello World4"), HW5("Hello World5"), HW6(
            "Hello World6"), HW7("Hello World7"), HW8("Hello World8"), HW9(
            "Hello World9"), HW10("Hello World10"), HW11("Hello World11"), HW12(
            "Hello World12"), HW13("Hello World13"), HW14("Hello World4"), HW15(
            "Hello World15");

    private String name = null;

    private HelloWorld(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void execute(String doSomething){
        doSomething = "Hello World0";
    }

    public static HelloWorld fromString(String input) {
        for (HelloWorld hw : HelloWorld.values()) {
            if (input.equals(hw.getName())) {
                return hw;
            }
        }
        return null;
    }

}





//Enum version for betterment on coding format compare to interface ExecutableClass
enum HelloWorld1 {
    HW0("Hello World0") {   
        public void execute(String doSomething){
            doSomething = "Hello World0";
        }
    }, 
    HW1("Hello World1"){    
        public void execute(String doSomething){
            doSomething = "Hello World0";
        }
    };
    private String name = null;

    private HelloWorld1(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void execute(String doSomething){
    //  super call, nothing here
    }
}


/*
 * http://stackoverflow.com/questions/338206/why-cant-i-switch-on-a-string
 * https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-3.html#jvms-3.10
 * http://forums.xkcd.com/viewtopic.php?f=11&t=33524
 */ 
Kanagavelu Sugumar
źródło
Time taken for String in Switch :3235 Time taken for String in if/else if :3143 Time taken for String in Map :4194 Time taken for String in ENUM :2866
halil
@halil Nie jestem pewien, jak ten kod działa w różnych środowiskach, ale wspomniałeś, że jeśli / elseif jest lepszy niż Switch i Map, nie jestem w stanie przekonać, ponieważ jeśli / elseif musi wykonywać więcej razy równa się porównaniu.
Kanagavelu Sugumar
2

W przypadku większości switchi większości if-then-elsebloków nie mogę sobie wyobrazić, że są jakieś zauważalne lub znaczące problemy związane z wydajnością.

Ale o to chodzi: jeśli używasz switchbloku, samo jego użycie sugeruje, że włączasz wartość pobraną z zestawu stałych znanych w czasie kompilacji. W takim przypadku naprawdę nie powinieneś w ogóle używać switchinstrukcji, jeśli możesz użyć enummetody z metodami specyficznymi dla stałej.

W porównaniu z switchinstrukcją wyliczenie zapewnia lepsze bezpieczeństwo typów i kod, który jest łatwiejszy w utrzymaniu. Wyliczenia można zaprojektować w taki sposób, aby po dodaniu stałej do zestawu stałych kod nie był kompilowany bez zapewnienia metody specyficznej dla stałej dla nowej wartości. Z drugiej strony zapomnienie o dodaniu nowego casedo switchbloku może czasami zostać złapane tylko w czasie wykonywania, jeśli masz szczęście, że ustawiłeś blok tak, aby zgłosił wyjątek.

Wydajność pomiędzy switchi enummetodą specyficzną dla stałej nie powinna się znacząco różnić, ale ta druga jest bardziej czytelna, bezpieczniejsza i łatwiejsza w utrzymaniu.

scottb
źródło