Вопрос

Какова наилучшая практика использования 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();
}
Это было полезно?

Решение

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

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

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

Другие советы

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

if (RequiresSpecialEvent(numError))
    fire_special_event();

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

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

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

Выключатель является Быстрее.

Просто попробуйте if/else использовать 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 ;
    }
}

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

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

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

Итак, в конечном итоге разработчику приходится выбирать между «скоростью + четкостью» и «быстростью + четкостью».«кодовая связь».

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

Изменить 21 сентября 2008 г.:

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

Конечно, это является.

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

.

Изменить 15 января 2013 г.:

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

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

The Switch, хотя бы для читабельности.На мой взгляд, гигантские операторы if труднее поддерживать и труднее читать.

ОШИБКА_01 :// намеренное проваливание

или

(ERROR_01 == numError) ||

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

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

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

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

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

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

Когда 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 с перемещением/сменой.Он компилирует 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'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()

Обратите внимание, как он вычитает 1 из errNumberlea совместить эту операцию с перемещением).Это позволяет ему вписать растровое изображение в 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 (до Bulldozer): Что означает слово «rep ret»?.Без него предсказание ветвей не будет работать так же хорошо на этих устаревших процессорах.

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

От Таблицы инструкций Агнера Фога, команда сдвига со счетчиком переменных работает медленнее, чем bt на последнем Intel (2 мопса вместо 1, и сдвиг не делает всего остального, что необходимо).

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

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

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

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

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

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

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

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

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

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

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

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 (особенно через 2-3 месяца)

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

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

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

Поскольку у вас всего 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);
}
}

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

Хотя если/иначе:Переключатель 5 мс:1 мс максимально петли:100000

Хотя если/иначе:Переключатель 5 мс:3 мс максимально петли:1000000

Хотя если/иначе:Переключатель 5 мс:14 мс максимально петли:10000000

Хотя если/иначе:Переключатель 5 мс:149 мс максимальные петли:100000000

(добавьте больше утверждений, если хотите)

Когда дело доходит до компиляции программы, не знаю, есть ли разница.Но что касается самой программы и максимально простого кода, лично я считаю, что это зависит от того, что вы хотите сделать.Операторы if else if 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";

Однако в спешке операторы If else if else могут стать сложными и запутанными (несмотря на все ваши попытки).Операторы 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.Если вы хотите протестировать отдельные значения или вам нужен понятный и легко читаемый блок, я бы рекомендовал вам использовать блоки case switch().

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top