Преимущество перехода на оператор if-else

Какая наилучшая практика для использования оператора switch vs с использованием оператора 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();
}

Ответы

Ответ 1

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

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

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

Ответ 2

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

if (RequiresSpecialEvent(numError))
    fire_special_event();

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

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

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

Ответ 3

Переключатель работает быстрее.

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

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

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

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

Большинство людей будут тогда использовать определения (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 ;
    }
}

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

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

Edit 2008-09-21:

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

Конечно, это так.

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

.

Edit 2013-01-15:

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

Ответ 4

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

Ответ 5

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

ERROR_01://преднамеренное падение

или

(ERROR_01 == numError) ||

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

Ответ 6

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

Ответ 7

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

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

Ответ 8

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

Я сделал несколько тестовых примеров на godbolt.

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

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

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

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

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

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 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 shlx to avoid moving the shift count into ecx
    testl   $2150662721, %eax
    jne .L10
.L5:
    rep ret
.L10:
    jmp fire_special_event()

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

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

    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

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

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

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

Из Таблицы инструкций Agner Fog, команда shift-count с переменной скоростью медленнее, чем bt на недавнем Intel (вместо 2 uops вместо 1, а сдвиг не делает все, что нужно).

Ответ 9

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

Ответ 10

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

Ответ 11

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

Ответ 12

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

Я предпочитаю, если выражения для операторов case, потому что они более читабельны и более гибкие - вы можете добавить другие условия, не основанные на числовом равенстве, например "|| max < min". Но для простого случая, который вы разместили здесь, это не имеет большого значения, просто сделайте то, что наиболее читаемо для вас.

Ответ 13

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

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

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

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

Ответ 14

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

Ответ 15

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

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

Ответ 16

while (true) != while (loop)

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

Ответ 17

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

Ответ 18

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

Ответ 19

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

Ответ 20

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

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

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

Ответ 21

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

Ответ 22

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

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

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

Пока, если /else: 5ms Переключатель: 1 мс Макс. Петли: 100000

Пока, если /else: 5ms Переключатель: 3 мс Макс. Петли: 1000000

Пока, если /else: 5ms Переключатель: 14 мс Макс. Петли: 10000000

Пока, если /else: 5ms Переключатель: 149 мс Макс. Петли: 100000000

(добавьте дополнительные инструкции, если хотите)

Ответ 23

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

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

(пример:

`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";

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

`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().