140

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

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

Это отредактировано как 'субъективное'? В самом деле? Разумеется, «субъективный» - это то, что невозможно доказать так или иначе? – 2008-09-18 23:42:19

+0

Уверен, что вы можете видеть его, с точки зрения которого генерируется наиболее эффективный код, но любой современный компилятор должен быть одинаково эффективным. В конце концов, это больше вопрос о цвете байка. – jfs 2008-09-18 23:59:09

+7

Я не согласен, я не думаю, что это субъективно. Простая разность ASM имеет значение, вы не можете просто игнорировать несколько секунд оптимизации во многих случаях.И в этом вопросе, это не религиозная война или дебаты, есть рациональное объяснение того, почему нужно быть быстрее, просто прочитайте принятый ответ. – chakrit 2008-09-24 18:53:25

ответ

140

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

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

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

+1

Технически все равно будет одно сравнение, чтобы убедиться, что значение перечисления находится в таблице прыжка. – 0124816 2008-09-19 06:12:53

+0

Jep. Это правда. Включение перечислений и обработка всех случаев может избавиться от последнего сравнения. – 2008-09-19 10:18:24

+0

Может ли кто-нибудь изменить это, чтобы потерять его? – 2008-09-24 19:02:06

18

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

+3

Скорее всего, компилятор не коснется if-then-else. На самом деле `gcc` не сделает этого точно (для этого есть веская причина). Clang оптимизирует оба случая в двоичном поиске. Например, см. [Это] (http://lazarenko.me/2013/01/13/switch-statement-machine-code/). – 2013-01-13 22:46:33

6

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

ERROR_01: // умышленное падение-через

или

(ERROR_01 == numError) ||

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

1

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

+0

ждать, ... что? – 2010-11-16 14:59:17

2

ИМО это прекрасный пример того, что переключение обходя переход был сделан для.

+0

в C# это единственный случай, когда происходит падение мысли. Хороший аргумент прямо там. – BCS 2008-09-18 23:40:58

0

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

1

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

0

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

0

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

1

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

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

1

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

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

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

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

0

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

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

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

1

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

0

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

18

Коммутатор является быстрее.

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

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

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

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

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

Edit 2008-09-21:

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

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

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

.

Edit 2013-01-15:

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

4

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

41

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

if (RequiresSpecialEvent(numError)) 
    fire_special_event(); 

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

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

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

1

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

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

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

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

0

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

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мс Max Loops: 100000

Хотя если/другое: 5мс Переключатель: 3ms Max Loops: +1000000

Хотя если/другое: 5мс Переключатель: 14ms Max Loops: 10000000

Хотя если/другое: 5мс Переключатель: 149ms Max Loops: 100000000

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

1
while (true) != while (loop) 

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

2

Компиляторы действительно хороши в оптимизации 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 с движением/сменой. Он компилирует версию 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 вездесущ, и a compiler bug)

rep ret используется в отрасли целей, и следующих условных ветвей, в пользу старых AMD K8 и K10 (добульдозер): What does `rep ret` mean?. Без него предсказание ветвлений не работает также и на этих устаревших процессорах.

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

От Agner Fog's instruction tables инструкция сдвига переменной счёта медленнее, чем bt на недавнем Intel (2 раза вместо 1, а смена не делает все остальное, что необходимо).

0

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

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

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

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

Смежные вопросы