Jeśli a prędkość przełączania

112

Instrukcje Switch są zazwyczaj szybsze niż równoważne instrukcje if-else-if (jak na przykład opisano w tym artykule ) ze względu na optymalizacje kompilatora.

Jak właściwie działa ta optymalizacja? Czy ktoś ma dobre wytłumaczenie?

Dirk Vollmar
źródło
3
Wyjaśnienie: stackoverflow.com/questions/395618/if-else-vs-switch
Matthew M. Osborn
Możliwa dobra odpowiedź: dotnetperls.com/if-switch-performance
Babak

Odpowiedzi:

185

Kompilator może tworzyć tabele skoków, jeśli ma to zastosowanie. Na przykład, kiedy użyjesz reflektora do spojrzenia na wyprodukowany kod, zobaczysz, że dla dużych przełączników na łańcuchach kompilator faktycznie wygeneruje kod, który używa tablicy skrótów do ich wysłania. Tablica skrótów używa ciągów jako kluczy i delegatów do casekodów jako wartości.

Ma to asymptotycznie lepszy czas wykonywania niż wiele iftestów łańcuchowych i jest w rzeczywistości szybsze nawet dla stosunkowo niewielu ciągów.

Konrad Rudolph
źródło
6
Dobra odpowiedź, interesująca kwestia tabeli skrótów.
BobbyShaftoe
4
W niektórych przypadkach konwertują one również na porównania drzew. Rozumowanie jest nieco skomplikowane, ale w zasadzie sprowadza się do neutralizacji w kierunku pośrednim tabeli buforów celu skoku nowoczesnego procesora, a tym samym usuwa predyktor gałęzi. Jak przez mgłę przypominam sobie artykuł z konferencji GCC na temat codegen dla przełączników.
olliej
To znaczy: switch (a) case "x": case "y": case "z": // coś się psuje; } jest szybsze niż: if (a == "x" || a == "b" || a == "c") // coś w porządku?
yazanpro
tutaj nie mamy zagnieżdżonych if else, tylko OR, więc co myślisz?
yazanpro
@yazanpro Na starych kompilatorach potencjalnie tak (ale pamiętaj, że liczba przypadków jest tak mała, że ​​może to nie mieć znaczenia!). Nowoczesne kompilatory wykonują jednak znacznie więcej analizy kodu. W rezultacie mogą dojść do wniosku, że te dwa fragmenty kodu są równoważne i zastosują te same optymalizacje. Ale to z mojej strony czysta spekulacja, nie wiem, czy jakiś kompilator faktycznie to robi.
Konrad Rudolph
15

Jest to niewielkie uproszczenie, ponieważ zwykle każdy nowoczesny kompilator, który napotyka if..else if ..sekwencję, którą można by w trywialny sposób przekonwertować na instrukcję switch przez osobę, kompilator również to zrobi. Ale tylko po to, aby zapewnić dodatkową zabawę, kompilator nie jest ograniczony składnią, więc może wewnętrznie generować instrukcje podobne do "przełączania", które mają mieszankę zakresów, pojedynczych celów itp. - i mogą (i robią) to zrobić zarówno dla switch, jak i if. .else.

W każdym razie rozszerzeniem odpowiedzi Konrada jest to, że kompilator może wygenerować tablicę skoków, ale niekoniecznie jest to gwarantowane (ani pożądane). Z różnych powodów tabele skoków źle wpływają na predyktory gałęzi na nowoczesnych procesorach, a same tabele źle wpływają na zachowanie pamięci podręcznej, np.

switch(a) { case 0: ...; break; case 1: ...; break; }

Gdyby kompilator faktycznie wygenerował dla tego tabelę skoków, prawdopodobnie byłby wolniejszy niż if..else if..kod alternatywnego stylu, ponieważ tabela skoków pokonałaby przewidywanie gałęzi.

olliej
źródło
4

Statystyki braku meczów mogą nie być dobre.

Jeśli faktycznie pobierasz źródło, wiadomo, że wartości no match to 21, zarówno w przypadku if, jak i switch. Kompilator powinien mieć możliwość abstrakcji, wiedząc, która instrukcja powinna być wykonywana przez cały czas, a procesor powinien być w stanie prawidłowo przewidywać rozgałęzienia.

Bardziej interesujący jest przypadek, gdy moim zdaniem nie każdy przypadek się załamuje, ale może nie był to zakres eksperymentu.

Calyth
źródło
4

Instrukcje switch / case mogą być zwykle szybsze o 1 poziom, ale gdy zaczniesz wchodzić w 2 lub więcej, instrukcje switch / case zaczynają zajmować 2-3 razy dłużej niż zagnieżdżone instrukcje if / else.

Ten artykuł zawiera porównania szybkości, które podkreślają różnice w szybkości w przypadku zagnieżdżania takich instrukcji.

Na przykład, zgodnie z ich testami, przykładowy kod podobny do poniższego:

if (x % 3 == 0)
            if (y % 3 == 0)
                total += 3;
            else if (y % 3 == 1)
                total += 2;
            else if (y % 3 == 2)
                total += 1;
            else
                total += 0;
        else if (x % 3 == 1)
            if (y % 3 == 0)
                total += 3;
            else if (y % 3 == 1)
                total += 2;
            else if (y % 3 == 2)
                total += 1;
            else
                total += 0;
        else if (x % 3 == 2)
            if (y % 3 == 0)
                total += 3;
            else if (y % 3 == 1)
                total += 2;
            else if (y % 3 == 2)
                total += 1;
            else
                total += 0;
        else
            if (y % 3 == 0)
                total += 3;
            else if (y % 3 == 1)
                total += 2;
            else if (y % 3 == 2)
                total += 1;
            else
                total += 0;

zakończyło się o połowę krócej niż wykonanie równoważnej instrukcji switch / case:

switch (x % 3)
    {
        case 0:
            switch (y % 3)
            {
                case 0: total += 3;
                    break;
                case 1: total += 2;
                    break;
                case 2: total += 1;
                    break;
                default: total += 0;
                    break;
            }
            break;
        case 1:
            switch (y % 3)
            {
                case 0: total += 3;
                    break;
                case 1: total += 2;
                    break;
                case 2: total += 1;
                    break;
                default: total += 0;
                    break;
            }
            break;
    case 2:
            switch (y % 3)
            {
                case 0: total += 3;
                    break;
                case 1: total += 2;
                    break;
                case 2: total += 1;
                    break;
                default: total += 0;
                    break;
            }
            break;
    default:
        switch (y % 3)
        {
            case 0: total += 3;
                break;
            case 1: total += 2;
                break;
            case 2: total += 1;
                break;
            default: total += 0;
                break;
        }
        break;
    }

Tak, to szczątkowy przykład, ale ilustruje to.

Więc wniosek może być taki, że użyj przełącznika / przypadku dla prostych typów, które mają tylko jeden poziom głębokości, ale w przypadku bardziej złożonych porównań i wielu zagnieżdżonych poziomów użyj klasycznych konstrukcji if / else?


źródło
-1: 1. Artykuł całkowicie zignorował Branch Prediction, 2. algorytmy nie są dokładnie takie same (pojedynczy if-else w łączu jest już zakodowany bardziej zoptymalizowany) i 3. znalezione różnice są tak małe, że nic nie usprawiedliwia użycie prawidłowego, czystego kodu (około 4 ns na 10.000.000 wywołań między przełącznikiem i tą samą konstrukcją if-else)
Trojaner
Ten przykład nie zostanie zoptymalizowany ze względu na to, jak niewiele przypadków ma blok przełączników. Zwykle po 5-6 elementach generuje tablicę skoków.
antiduh
0

Jedyną zaletą przypadku if over jest zauważalny wzrost częstości występowania pierwszego przypadku.

Nie wiem dokładnie, gdzie jest próg, ale używam składni wielkości liter, chyba że pierwszy „prawie zawsze” przejdzie pierwszy test.

Ralph
źródło