Преимущество переключения оператора if-else - PullRequest
157 голосов
/ 19 сентября 2008

Как лучше всего использовать оператор 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 ]

150 голосов
/ 19 сентября 2008

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

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

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

42 голосов
/ 25 сентября 2008

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

if (RequiresSpecialEvent(numError))
    fire_special_event();

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

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

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

19 голосов
/ 19 сентября 2008

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

Просто попробуйте, если / еще 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 ;
    }
}

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

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

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

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

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

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

.

Редактировать 2013-01-15:

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

18 голосов
/ 19 сентября 2008

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

6 голосов
/ 19 сентября 2008

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

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

или

(ERROR_01 == numError) ||

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

5 голосов
/ 24 сентября 2008

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

5 голосов
/ 25 сентября 2008

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

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

3 голосов
/ 02 сентября 2015

Компиляторы действительно хороши в оптимизации 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 с 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'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 (pre-Bulldozer): Что означает `rep ret`? . Без этого предсказание ветвлений не работает так же хорошо на этих устаревших процессорах.

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

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

2 голосов
/ 19 сентября 2008

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

1 голос
/ 19 сентября 2008

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...