Преимущество оператора switch over if-else

какова наилучшая практика использования switch заявление против использования if заявление для 30 unsigned перечисления, где около 10 имеют ожидаемое действие (это в настоящее время то же самое действие). Производительность и космические должны быть рассмотрены, но не являются критическими. Я абстрагировал фрагмент, поэтому не ненавидьте меня за соглашения об именах.

switch о себе:

// 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 о себе:

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();
}

23 ответов


используйте переключатель.

в худшем случае компилятор будет генерировать тот же код, что и цепочка if-else, поэтому вы ничего не потеряете. Если сомневаетесь, сначала поместите наиболее распространенные случаи в оператор switch.

в лучшем случае оптимизатор может найти лучший способ для генерации кода. Обычно компилятор создает двоичное дерево решений (сохраняет сравнения и прыжки в среднем случае) или просто создает таблицу переходов (работает без сравнения вообще).


для особого случая, который вы указали в своем примере, самый ясный код, вероятно:

if (RequiresSpecialEvent(numError))
    fire_special_event();

очевидно, что это просто переносит проблему в другую область кода, но теперь у вас есть возможность использовать этот тест. У вас также есть больше вариантов, как ее решить. Вы можете использовать std:: set, например:

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

Я не предполагаю, что это лучшая реализация RequiresSpecialEvent, просто это вариант. Вы все еще можете использовать переключатель или цепочка if-else, или таблица подстановки, или некоторые битовые манипуляции со значением, что угодно. Чем более неясным становится ваш процесс принятия решений, тем большую ценность вы получите от его изолированной функции.


переключатель is быстрее.

просто попробуйте if / else-ing 30 различных значений внутри цикла и сравните его с тем же кодом, используя switch, чтобы увидеть, насколько быстрее переключатель.

и переключатель имеет одну реальную проблему : коммутатор должен знать во время компиляции значения внутри каждого случая. Это означает, что следующий код:

// 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 ;
    }
}

не скомпилируется.

большинство людей будут затем использовать defines (Aargh!), и другие объявлять и определять постоянные переменные в одной и той же единице компиляции. Например:

// 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 ;
    }
}

Так, в конце концов, разработчик должен выбрать между "скоростью + ясностью "против" соединения кода".

(не то, что коммутатор не может быть записан, чтобы быть запутанным, как ад... Большинство коммутаторов, которые я сейчас вижу, относятся к этой" запутанной"категории"... Но это уже другая история...)

изменить 2008-09-21:

bk1e добавлена следующий комментарий:"определение констант как перечислений в заголовочном файле-это еще один способ справиться с этим".

конечно.

точка внешнего типа должна была отделить значение от источника. Определение этого значения как макроса, как простого объявления const int или даже как перечисления имеет побочный эффект вставки значения. Таким образом, если значение define, enum или const int изменится, потребуется перекомпиляция. В Экстерн объявление означает, что нет необходимости перекомпилировать в случае изменения значения, но, с другой стороны, делает невозможным использование switch. Вывод был использование переключателя увеличит связь между кодом переключателя и переменными, используемыми в качестве случаев. Когда все в порядке, используйте переключатель. Когда это не так, тогда неудивительно.

.

изменить 2013-01-15:

Влад Лазаренко прокомментировал мой ответ, дающий ссылку на его углубленное изучение кода сборки, генерируемого коммутатором. Очень enlightning: http://741mhz.com/switch/


компилятор оптимизирует его в любом случае-перейдите к коммутатору, поскольку он наиболее читаемый.


переключатель, если только для удобочитаемости. Гигантские, если заявления сложнее поддерживать и сложнее читать, на мой взгляд.

ERROR_01 : // преднамеренное провал

или

(ERROR_01 == numError)//

более поздняя версия более подвержена ошибкам и требует больше ввода и форматирования, чем первая.


используйте переключатель, это то, для чего он предназначен и чего ожидают программисты.

Я бы поставил избыточные ярлыки случаев, хотя-просто чтобы люди чувствовали себя комфортно, я пытался вспомнить, когда / какие правила для их исключения.
Вы не хотите, чтобы следующий программист, работающий над ним, делал ненужные мысли о языковых деталях (это может быть вы через несколько месяцев!)


код для удобочитаемости. Если вы хотите знать, что работает лучше, используйте профилировщик, поскольку оптимизации и компиляторы различаются, а проблемы производительности редко там, где люди думают.


IMO это идеальный пример того, для чего был сделан переключатель.


компиляторы очень хорошо оптимизируют switch. Недавний gcc также хорошо оптимизирует кучу условий в if.

я сделал несколько тестов на godbolt.

когда case значения сгруппированы близко друг к другу, gcc, clang и icc достаточно умны, чтобы использовать растровое изображение, чтобы проверить, является ли значение одним из специальных.

например, gcc 5.2-O3 компилирует switch to (и if что-то очень похожие):

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

обратите внимание, что растровое изображение-это непосредственные данные, поэтому нет потенциального пропускания кэша данных или таблицы перехода.

gcc 4.9.2-O3 компилирует switch для растрового изображения, но делает 1U<<errNumber С mov / shift. Он компилирует if версия к серии ветвей.

errhandler_switch(errtype):  # gcc 4.9.2 -O3
    leal    -1(%rdi), %ecx
    cmpl    , %ecx    # cmpl , %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    , %eax
    salq    %cl, %rax   # with -march=haswell, it will use BMI's shlx to avoid moving the shift count into ecx
    testl   50662721, %eax
    jne .L10
.L5:
    rep ret
.L10:
    jmp fire_special_event()

обратите внимание, как он вычитает 1 из errNumberlea объединить эту операцию с движением). Это позволяет ему поместить растровое изображение в 32bit немедленно, избегая 64bit-immediate movabsq который принимает больше байтов инструкции.

более короткая (в машинном коде) последовательность будет:

    cmpl    , %edi
    ja  .L5
    mov     50662721, %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

(неспособность использовать jc fire_special_event вездесущ, и компилятор ошибка.)

rep ret используется в целевых ветвях и следующих условных ветвях в интересах старых AMD K8 и K10 (pre-Bulldozer):что означает "rep ret"?. Без него прогнозирование ветвей не работайте также над этими устаревшими процессорами.

bt (тест бита) с arg регистра быстр. Он сочетает в себе работу левого сдвига 1 на errNumber биты и делаю test, но все еще 1 задержка цикла и только один Intel uop. Он медленный с arg памяти из-за его семантики way-too-CISC: с операндом памяти для "битовой строки" адрес проверяемого байта вычисляется на основе другого arg (разделенного на 8) и не ограничивается куском 1, 2, 4 или 8byte на что указывает операнд памяти.

С таблицы инструкций Agner Fog, переменная-count shift инструкция медленнее, чем bt на недавнем Intel (2 uops вместо 1, и shift не делает все остальное, что необходимо).


Если ваши случаи, вероятно, останутся сгруппированными в будущем-если более одного случая соответствует одному результату-переключатель может оказаться легче читать и поддерживать.


Они работают одинаково хорошо. Производительность примерно такая же, учитывая современный компилятор.

Я предпочитаю операторы if над операторами case, потому что они более читабельны и более гибки-вы можете добавить другие условия, не основанные на числовом равенстве, например " || max


переключатель, безусловно, предпочтительнее. Легче посмотреть на список случаев коммутатора и точно знать, что он делает, чем читать длинное условие if.

дублирование в if состояние трудно на глазах. Предположим, один из == было написано !=; вы заметили? Или если один экземпляр "numError" был написан "nmuError", который просто случайно компилировался?

Я вообще предпочитаю использовать полиморфизм вместо переключателя, но не более детали контекста, трудно сказать.

что касается производительности, лучше всего использовать профилировщик для измерения производительности вашего приложения в условиях, похожих на то, что вы ожидаете в дикой природе. В противном случае вы, вероятно, оптимизируетесь не в том месте и не в том направлении.


Я согласен с компактностью решения коммутатора, но IMO вы захват переключатель здесь.
Цель переключателя-иметь разные обращение в зависимости от значения.
Если бы вам пришлось объяснить свой algo в псевдокоде, вы бы использовали if, потому что семантически это так:если whatever_error сделать это...
Поэтому, если вы не собираетесь когда-нибудь изменить свой код, чтобы иметь определенный код для каждой ошибки, я бы использовал если.


Я не уверен в лучшей практике, но я бы использовал switch-а затем ловушку преднамеренного провала через "default"


эстетически я склоняюсь к такому подходу.

unsigned int special_events[] = {
    ERROR_01,
    ERROR_07,
    ERROR_0A,
    ERROR_10,
    ERROR_15,
    ERROR_16,
    ERROR_20
 };
 int special_events_length = sizeof (special_events) / sizeof (unsigned int);

 void process_event(unsigned int numError) {
     for (int i = 0; i < special_events_length; i++) {
         if (numError == special_events[i]) {
             fire_special_event();
             break;
          }
     }
  }

сделайте данные немного умнее, чтобы мы могли сделать логику немного тупее.

Я понимаю, что это выглядит странно. Вот вдохновение (от того, как я бы это сделал в Python):

special_events = [
    ERROR_01,
    ERROR_07,
    ERROR_0A,
    ERROR_10,
    ERROR_15,
    ERROR_16,
    ERROR_20,
    ]
def process_event(numError):
    if numError in special_events:
         fire_special_event()

while (true) != while (loop)

вероятно, первый оптимизирован компилятором, что объясняет, почему второй цикл медленнее при увеличении количества циклов.


Я бы выбрал утверждение if ради ясности и конвенции, хотя я уверен, что некоторые не согласятся. В конце концов, вы хотите что-то сделать if некоторые условия! Иметь переключатель с одним действием кажется немного... ненужную.


пожалуйста, используйте переключатель. Оператор if займет время, пропорциональное количеству условий.


Im не тот человек, чтобы рассказать вам о скорости и использовании памяти, но глядя на состояние коммутатора намного проще понять, чем большой оператор if (особенно 2-3 месяца вниз по линии)


Я бы сказал, используйте переключатель. Таким образом, вам нужно только реализовать различные результаты. Ваши десять одинаковых случаев могут использовать значение по умолчанию. Если одно изменение все, что вам нужно, это явно реализовать изменение, нет необходимости редактировать значение по умолчанию. Также намного проще добавлять или удалять случаи из коммутатора, чем редактировать IF и ELSEIF.

switch(numerror){
    ERROR_20 : { fire_special_event(); } break;
    default : { null; } break;
}

возможно, даже Проверьте свое условие (в данном случае numerror) на список возможностей, массив, возможно, поэтому ваш коммутатор даже не используется, если нет определенно будет результат.


видя, что у вас есть только 30 кодов ошибок, кодируйте свою собственную таблицу переходов, затем вы сами делаете все варианты оптимизации (переход всегда будет самым быстрым), а не надеетесь, что компилятор сделает все правильно. Это также делает код очень маленьким (кроме статического объявления таблицы перехода). Он также имеет побочное преимущество, что с отладчиком вы можете изменить поведение во время выполнения, если вам это нужно, просто тыча данные таблицы напрямую.


Я знаю его старый, но

public class SwitchTest {
static final int max = 100000;

public static void main(String[] args) {

int counter1 = 0;
long start1 = 0l;
long total1 = 0l;

int counter2 = 0;
long start2 = 0l;
long total2 = 0l;
boolean loop = true;

start1 = System.currentTimeMillis();
while (true) {
  if (counter1 == max) {
    break;
  } else {
    counter1++;
  }
}
total1 = System.currentTimeMillis() - start1;

start2 = System.currentTimeMillis();
while (loop) {
  switch (counter2) {
    case max:
      loop = false;
      break;
    default:
      counter2++;
  }
}
total2 = System.currentTimeMillis() - start2;

System.out.println("While if/else: " + total1 + "ms");
System.out.println("Switch: " + total2 + "ms");
System.out.println("Max Loops: " + max);

System.exit(0);
}
}

изменение количества циклов сильно меняется:

пока if / else: 5ms Переключатель: 1мс Максимум Циклов: 100000

пока if / else: 5ms Переключения: 3 мс Максимум Циклов: 1000000

пока if / else: 5ms Переключатель: 14 мс Макс Циклы: 10000000

пока if / else: 5ms Переключатель: 149ms Максимум Циклов: 100000000

(добавьте больше операторов, если хотите)


когда дело доходит до компиляции программы, я не знаю, есть ли какая-либо разница. Но что касается самой программы и сохранения кода максимально простым, я лично думаю, что это зависит от того, что вы хотите сделать. если еще, если еще утверждения имеют свои преимущества, которые я думаю:

позволяет протестировать переменную в определенных диапазонах вы можете использовать функции (стандартную библиотеку или Personal) в качестве условий.

(например:

`int a;
 cout<<"enter value:\n";
 cin>>a;

 if( a > 0 && a < 5)
   {
     cout<<"a is between 0, 5\n";

   }else if(a > 5 && a < 10)

     cout<<"a is between 5,10\n";

   }else{

       "a is not an integer, or is not in range 0,10\n";

однако, если еще если еще заявления могут стать сложными и грязными (несмотря на ваши лучшие попытки) в спешке. Операторы Switch, как правило, яснее, чище и легче читать; но может использоваться только для тестирования против определенных значений (пример:

`int a;
 cout<<"enter value:\n";
 cin>>a;

 switch(a)
 {
    case 0:
    case 1:
    case 2: 
    case 3:
    case 4:
    case 5:
        cout<<"a is between 0,5 and equals: "<<a<<"\n";
        break;
    //other case statements
    default:
        cout<<"a is not between the range or is not a good value\n"
        break;

Я предпочитаю утверждения if-else if-else, но это действительно зависит от вас. Если вы хотите использовать функции в качестве условий или хотите протестировать что-то против диапазона, массива или вектора и/или вы не возражаете иметь дело со сложным вложением, я бы рекомендуется использовать if else if else blocks. Если вы хотите протестировать против одиночных значений или хотите чистый и легко читаемый блок, я бы рекомендовал вам использовать блоки switch() case.