Pytanie Zaleta przełączania instrukcji if-else


Jaka jest najlepsza praktyka używania a switch oświadczenie kontra użycie if oświadczenie za 30 unsigned wyliczenia, w których około 10 ma oczekiwane działanie (obecnie jest to to samo działanie). Wydajność i przestrzeń muszą być brane pod uwagę, ale nie są krytyczne. Wyjaśniłem fragment, więc nie nienawidzę mnie za konwencje nazewnictwa.

switch komunikat:

// numError is an error enumeration type, with 0 being the non-error case
// fire_special_event() is a stub method for the shared processing

switch (numError)
{  
  case ERROR_01 :  // intentional fall-through
  case ERROR_07 :  // intentional fall-through
  case ERROR_0A :  // intentional fall-through
  case ERROR_10 :  // intentional fall-through
  case ERROR_15 :  // intentional fall-through
  case ERROR_16 :  // intentional fall-through
  case ERROR_20 :
  {
     fire_special_event();
  }
  break;

  default:
  {
    // error codes that require no additional action
  }
  break;       
}

if komunikat:

if ((ERROR_01 == numError)  ||
    (ERROR_07 == numError)  ||
    (ERROR_0A == numError)  || 
    (ERROR_10 == numError)  ||
    (ERROR_15 == numError)  ||
    (ERROR_16 == numError)  ||
    (ERROR_20 == numError))
{
  fire_special_event();
}

143
2017-09-18 23:28


pochodzenie


To jest edytowane jako "subiektywne"? Naprawdę? Z pewnością "subiektywny" dotyczy rzeczy, których nie można udowodnić w taki czy inny sposób? - Alexandra Franks
Oczywiście możesz go zobaczyć od strony, która generuje najbardziej wydajny kod, ale każdy nowoczesny kompilator powinien być równie wydajny. Ostatecznie jest to bardziej kwestia koloru szopy na rowery. - jfs
Nie zgadzam się, nie sądzę, że to jest subiektywne. Prosta różnica ASM ma znaczenie, w wielu przypadkach nie można pominąć kilku sekund optymalizacji. I w tym pytaniu nie jest to wojna religijna czy debata, istnieje racjonalne wytłumaczenie, dlaczego można być szybszym, wystarczy przeczytać zaakceptowaną odpowiedź. - chakrit
Który jest szybszy: stackoverflow.com/questions/6805026/is-switch-faster-than-if - Ciro Santilli 新疆改造中心 六四事件 法轮功
@RichardFranks: Offtopic: grats! jesteś pierwszą osobą, która przejęła umiar na tyle, ile kiedykolwiek widziałem - jungle_mole


Odpowiedzi:


Użyj przełącznika.

W najgorszym przypadku kompilator wygeneruje ten sam kod co łańcuch if-else, więc nic nie stracisz. W razie wątpliwości, najpopularniejsze przypadki należy najpierw zamieścić w instrukcji przełączania.

W najlepszym przypadku optymalizator może znaleźć lepszy sposób generowania kodu. Najczęściej kompilatorem jest budowanie binarnego drzewa decyzyjnego (zapisuje porównania i przeskakuje w przeciętnym przypadku) lub po prostu buduje tabelę skoków (działa bez porównania).


142
2017-09-18 23:32



Technicznie nadal będzie jeden porównać, aby upewnić się, że wartość wyliczenia znajduje się w tabeli skoku. - 0124816
Jep. To prawda. Włączanie wyliczeń i obsługa wszystkich przypadków może jednak pozbyć się ostatniego porównania. - Nils Pipenbrinck
Zauważ, że seria ifów mogłaby teoretycznie zostać zanalizowana tak, aby była taka sama jak przełączenie przez kompilator, ale dlaczego skorzystać? Używając przełącznika, komunikujesz dokładnie to, czego potrzebujesz, co ułatwia generowanie kodu. - jakobengblom2
jakoben: Można to zrobić, ale tylko dla switch-like, jeśli / else łańcuchy. W praktyce nie występują one, ponieważ programiści używają przełącznika. Wkopałem się w technologię kompilacji i zaufaj mi: znalezienie takich "bezużytecznych" konstrukcji zajmuje dużo czasu. Dla kompilatorów taka optymalizacja ma sens. - Nils Pipenbrinck
@NilsPipenbrinck z łatwością budowania pseudorekursywnego if-else łańcuchy w programowaniu meta szablonów i trudność generowania switch  case łańcuchy, że mapowanie może stać się ważniejsze. (i tak, starożytny komentarz, ale sieć jest na zawsze, a przynajmniej do następnego wtorek) - Yakk - Adam Nevraumont


W przypadku szczególnego przypadku podanego w przykładzie najczystszy kod prawdopodobnie:

if (RequiresSpecialEvent(numError))
    fire_special_event();

Oczywiście to po prostu przenosi problem na inny obszar kodu, ale teraz masz możliwość ponownego użycia tego testu. Masz również więcej opcji, jak go rozwiązać. Możesz użyć std :: set, na przykład:

bool RequiresSpecialEvent(int numError)
{
    return specialSet.find(numError) != specialSet.end();
}

Nie sugeruję, że jest to najlepsza implementacja RequestsSpecialEvent, tylko że jest to opcja. Nadal możesz używać przełącznika lub łańcucha-else lub tablicy odnośników lub jakiejś manipulacji bitowej na wartości, cokolwiek. Im bardziej niejasny staje się proces decyzyjny, tym większą wartość czerpiesz z posiadania go w odizolowanej funkcji.


42
2017-09-24 20:01



To prawda. Czytelność jest o wiele lepsza niż instrukcje switch i if. Właściwie to chciałem odpowiedzieć na coś takiego, ale mnie biłeś. :-) - mlarsen
Jeśli twoje wartości wyliczeniowe są małe, to nie potrzebujesz hasha, tylko tabeli. na przykład const std::bitset<MAXERR> specialerror(initializer);  Użyj go z if (specialerror[numError]) { fire_special_event(); }. Jeśli chcesz sprawdzić granice, bitset::test(size_t)rzuci wyjątek na wartości poza zakresem. (bitset::operator[] nie sprawdza zasięgu). cplusplus.com/reference/bitset/bitset/test. Prawdopodobnie będzie to lepsze niż implementacja tabeli przeskoków generowanych przez kompilator switch, szczególnie. w nietypowym przypadku, w którym będzie to pojedyncza nie brana gałąź. - Peter Cordes
@PeterCordes Nadal twierdzę, że lepiej jest umieścić tabelę we własnej funkcji. Jak już powiedziałem, są dużo opcji, które otwierają się, gdy to robisz, nie próbowałem wyliczyć ich wszystkich. - Mark Ransom
@MarkRansom: Nie chciałem się z tym nie zgadzać. Od momentu, w którym zaimplementowałeś przykładową implementację std::set, Myślałem, że wskażę, że to chyba zły wybór. Okazuje się, że gcc już kompiluje kod OP, aby przetestować bitmapę w trybie natychmiastowym 32-bitowym. godbolt: goo.gl/qjjv0e. gcc 5.2 zrobi to nawet dla if wersja. Ponadto nowszy gcc użyje instrukcji testu bitowego bt zamiast przesuwać, aby umieścić 1 bit w odpowiednim miejscu i przy użyciu test reg, imm32. - Peter Cordes
Ta natychmiastowa stała mapa bitowa jest wielką wygraną, ponieważ w bitmapach nie brakuje pamięci podręcznej. Działa, jeśli "specjalne" kody błędów mieszczą się w zakresie 64 lub mniej. (lub 32 dla starego 32-bitowego kodu.) Kompilator odejmuje najmniejszą wartość wielkości, jeśli jest niezerowa. Na wynos jest to, że najnowsze kompilatory są wystarczająco inteligentne, że prawdopodobnie otrzymasz dobry kod z dowolnej logiki, której używasz, chyba że powiesz mu, aby używał dużej struktury danych. - Peter Cordes


Przełącznik jest szybciej.

Po prostu spróbuj jeśli / else-w 30 różnych wartości w pętli, i porównać go do tego samego kodu za pomocą przełącznika, aby zobaczyć, o ile szybszy jest przełącznik.

Teraz przełącznik ma jeden prawdziwy problem : Przełącznik musi wiedzieć podczas kompilacji wartości w każdym przypadku. Oznacza to, że poniższy kod:

// WON'T COMPILE
extern const int MY_VALUE ;

void doSomething(const int p_iValue)
{
    switch(p_iValue)
    {
       case MY_VALUE : /* do something */ ; break ;
       default : /* do something else */ ; break ;
    }
}

nie będzie się kompilować.

Większość ludzi będzie wtedy używać definicji (Aargh!), A inni będą zadeklarować i zdefiniować stałe zmienne w tej samej jednostce kompilacji. Na przykład:

// WILL COMPILE
const int MY_VALUE = 25 ;

void doSomething(const int p_iValue)
{
    switch(p_iValue)
    {
       case MY_VALUE : /* do something */ ; break ;
       default : /* do something else */ ; break ;
    }
}

Ostatecznie program musi wybrać pomiędzy "szybkością + klarownością" a "łączeniem kodu".

(Nie chodzi o to, że przełącznik nie może być napisany jako mylący jak cholera ... Większość przełącznika, jaki obecnie widzę, jest tej "mylącej" kategorii "... Ale to już inna historia ...)

Edycja 2008-09-21:

bk1e dodano następujący komentarz: "Definiowanie stałych jako wyliczeń w pliku nagłówkowym jest innym sposobem radzenia sobie z tym ".

Oczywiście, że jest.

Punktem typu zewnętrznego było oddzielenie wartości od źródła. Zdefiniowanie tej wartości jako makra, jako prostej deklaracji const int lub nawet jako wyliczenia ma efekt uboczny polegający na zaznaczeniu wartości. Tak więc, jeśli zmieni się wartość definiująca, wartość wyliczeniowa lub wartość const, konieczna będzie rekompilacja. Deklaracja zewnętrzna oznacza, że ​​nie ma potrzeby rekompilacji w przypadku zmiany wartości, ale z drugiej strony uniemożliwia użycie przełącznika. Wniosek jest Użycie przełącznika zwiększy sprzężenie między kodem przełącznika a zmiennymi stosowanymi jako przypadki. Kiedy jest OK, użyj przełącznika. Jeśli tak nie jest, nie ma w tym nic dziwnego.

.

Edycja 2013-01-15:

Vlad Lazarenko skomentował moją odpowiedź, podając link do jego dogłębnej analizy kodu zespołu generowanego przez przełącznik. Bardzo pouczające: http://741mhz.com/switch/


19
2017-09-19 16:12



Definiowanie stałych jako wyliczeń w pliku nagłówkowym jest innym sposobem radzenia sobie z tym. - bk1e
Przełącznik to nie zawsze szybciej.
@Vlad Lazarenko: Dzięki za link! To była bardzo interesująca lektura. - paercebal


Kompilator i tak to zoptymalizuje - wybierz przełącznik, ponieważ jest on najbardziej czytelny.


18
2017-09-18 23:30



Możliwe, że kompilator nie dotknie, jeśli-następnie-else. W rzeczywistości, gcc nie zrobię tego na pewno (jest ku temu dobry powód). Clang zoptymalizuje oba przypadki do wyszukiwania binarnego. Na przykład zobacz to.


Przełącznik, jeśli tylko dla czytelności. Giant, jeśli stwierdzenia są trudniejsze do utrzymania i trudniejsze do odczytania w mojej opinii.

ERROR_01 : // zamierzony upadek

lub

(ERROR_01 == numError) ||

Później jest bardziej podatny na błędy i wymaga więcej pisania i formatowania niż pierwszy.


6
2017-09-18 23:30





Użyj przełącznika, do czego służy i jakich programistów się spodziewasz.

Wrzuciłbym jednak niepotrzebne etykiety na walizki - po to, aby ludzie czuli się dobrze, starałem się zapamiętać, kiedy / jakie zasady są dla ich opuszczenia.
Nie chcesz, aby następny programista pracujący nad nim musiał niepotrzebnie myśleć o szczegółach języka (może to być za kilka miesięcy!)


5
2017-09-24 20:54





Kod czytelności. Jeśli chcesz wiedzieć, co działa lepiej, użyj profilera, ponieważ optymalizacje i kompilatory są różne, a problemy z wydajnością rzadko kiedy ludzie myślą, że tak.


4
2017-09-24 19:26





IMO to doskonały przykład tego, do czego został stworzony przełom.


2
2017-09-18 23:31



w języku c # jest to jedyny przypadek, w którym następuje spadek myśli. Dobra argumentacja. - BCS


Kompilatory są naprawdę dobre w optymalizacji switch. Najnowszy gcc jest również dobry w optymalizacji wielu warunków w if.

Zrobiłem kilka przypadków testowych Godbolt.

Kiedy case wartości są pogrupowane blisko siebie, gcc, clang i icc są na tyle sprytne, aby użyć bitmapy do sprawdzenia, czy wartość jest jedną z wartości specjalnych.

na przykład gcc 5.2 -O3 kompiluje switch do (i if coś bardzo podobnego):

errhandler_switch(errtype):  # gcc 5.2 -O3
    cmpl    $32, %edi
    ja  .L5
    movabsq $4301325442, %rax   # highest set bit is bit 32 (the 33rd bit)
    btq %rdi, %rax
    jc  .L10
.L5:
    rep ret
.L10:
    jmp fire_special_event()

Zauważ, że bitmapa jest natychmiastowa, więc nie ma możliwości utraty dostępu do pamięci podręcznej danych lub tabeli przeskoku.

gcc 4.9.2 -O3 kompiluje switch do bitmapy, ale robi to 1U<<errNumber z mov / shift. To kompiluje if wersja do serii oddziałów.

errhandler_switch(errtype):  # gcc 4.9.2 -O3
    leal    -1(%rdi), %ecx
    cmpl    $31, %ecx    # cmpl $32, %edi  wouldn't have to wait an extra cycle for lea's output.
              # However, register read ports are limited on pre-SnB Intel
    ja  .L5
    movl    $1, %eax
    salq    %cl, %rax   # with -march=haswell, it will use BMI's shlx to avoid moving the shift count into ecx
    testl   $2150662721, %eax
    jne .L10
.L5:
    rep ret
.L10:
    jmp fire_special_event()

Zwróć uwagę, jak odejmuje 1 od errNumber (z lea połączyć tę operację z ruchem). To pozwala dopasować bitmapę do 32-bitowej natychmiastowej, unikając natychmiastowego 64-bitowego movabsq co zabiera więcej bajtów instrukcji.

Krótsza (w kodzie maszynowym) sekwencja będzie:

    cmpl    $32, %edi
    ja  .L5
    mov     $2150662721, %eax
    dec     %edi   # movabsq and btq is fewer instructions / fewer Intel uops, but this saves several bytes
    bt     %edi, %eax
    jc  fire_special_event
.L5:
    ret

(Niepowodzenie użycia jc fire_special_event jest wszechobecny i jest błąd kompilatora.)

rep ret jest używany w oddziałach docelowych i następujących gałęziach warunkowych, na korzyść starych AMD K8 i K10 (pre-Bulldozer): Co znaczy "rep ret"?. Bez tego prognozy rozgałęzień nie działają tak dobrze na tych przestarzałych procesorach.

bt (bit test) z rejestrem arg jest szybki. Łączy pracę przesunięcia w lewo o 1 errNumber bitów i robienia test, ale wciąż jest 1-krotne opóźnienie i tylko jeden procesor Intel. Jest powolny z argumentem pamięci ze względu na jego sposób-zbyt-semantyki CISC: z operandem pamięci dla "ciągu bitów", adres bajtu, który ma być testowany, jest obliczany na podstawie drugiego argumentu (podzielonego przez 8), a isn ograniczone do 1, 2, 4 lub 8-bajtowego fragmentu wskazywanego przez operand pamięci.

Od Tabele instrukcji Agner Fog, instrukcja zmiany zmiennej jest wolniejsza niż a bt na najnowszym Intel (2 upy zamiast 1, a shift nie robi wszystkiego, co jest potrzebne).


2
2017-09-02 14:37





Jeśli Twoje przypadki prawdopodobnie zostaną pogrupowane w przyszłości - jeśli więcej niż jeden przypadek odpowiada jednemu wynikowi - zmiana może okazać się łatwiejsza do odczytania i utrzymania.


1
2017-09-18 23:31





Działają równie dobrze. Wydajność jest prawie taka sama, biorąc pod uwagę nowoczesny kompilator.

Wolę, jeśli wyrażenia w porównaniu z orzeczeniami spraw, ponieważ są bardziej czytelne i bardziej elastyczne - można dodać inne warunki nie oparte na równości numerycznej, takie jak "|| max <min". Ale w tym prostym przypadku, który tutaj zamieściłeś, nie ma to znaczenia, po prostu rób to, co jest dla ciebie najbardziej czytelne.


1
2017-09-18 23:33