Vantagem de mudar a declaração if-else

Qual é a melhor prática para usar uma instrução switch vs usando uma instrução if para 30 enumerações unsigned onde cerca de 10 têm uma ação esperada (que atualmente é a mesma ação). Desempenho e espaço precisam ser considerados, mas não são críticos. Eu abstraí o trecho, então não me odeie pelas convenções de nomenclatura.

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 declaração:

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

Use o interruptor.

No pior dos casos, o compilador irá gerar o mesmo código que uma cadeia if-else, para que você não perca nada. Em caso de dúvida, coloque os casos mais comuns primeiro na instrução switch.

No melhor dos casos, o otimizador pode encontrar uma maneira melhor de gerar o código. Coisas comuns que um compilador faz é construir uma tree de decisão binária (salva compara e salta no caso médio) ou simplesmente constrói uma tabela de salto (funciona sem comparações).

Para o caso especial que você forneceu em seu exemplo, o código mais claro é provavelmente:

 if (RequiresSpecialEvent(numError)) fire_special_event(); 

Obviamente isso apenas move o problema para uma área diferente do código, mas agora você tem a oportunidade de reutilizar esse teste. Você também tem mais opções para resolvê-lo. Você poderia usar std :: set, por exemplo:

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

Não estou sugerindo que essa seja a melhor implementação do RequiresSpecialEvent, apenas que é uma opção. Você ainda pode usar um switch ou cadeia if-else, ou uma tabela de consulta, ou alguma manipulação de bits no valor, qualquer que seja. Quanto mais obscuro for o seu processo decisório, mais valor você obterá se tiver uma function isolada.

O comutador é mais rápido.

Apenas tente se / senão 30 valores diferentes dentro de um loop, e compare com o mesmo código usando o switch para ver o quão mais rápido o switch é.

Agora, o switch tem um problema real : o switch deve saber em tempo de compilation os valores dentro de cada caso. Isso significa que o código a seguir:

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

não compilará.

A maioria das pessoas usará define (Aargh!), E outras irão declarar e definir variables ​​constantes na mesma unidade de compilation. Por exemplo:

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

Então, no final, o desenvolvedor deve escolher entre “velocidade + clareza” versus “acoplamento de código”.

(Não que um interruptor não pode ser escrito para ser confuso como o inferno … A maioria da mudança que vejo atualmente são desta categoria “confusa” “… Mas isso é outra história …)

Editar 2008-09-21:

bk1e adicionou o seguinte comentário: ” Definir constantes como enums em um arquivo de header é outra maneira de lidar com isso”.

Claro que é.

O ponto de um tipo externo era dissociar o valor da fonte. Definir este valor como uma macro, como uma simples declaração de const int, ou até mesmo como um enum tem o efeito colateral de inlining o valor. Portanto, se a definição, o valor enum ou o valor const int forem alterados, será necessária uma recompilation. A declaração externa significa que não há necessidade de recompilar em caso de alteração de valor, mas, por outro lado, torna impossível usar o switch. A conclusão que está sendo usando o switch aumentará o acoplamento entre o código do switch e as variables ​​usadas como casos . Quando estiver Ok, use o switch. Quando não é, então, não é surpresa.

.

Editar 2013-01-15:

Vlad Lazarenko comentou a minha resposta, dando um link para seu estudo em profundidade do código de assembly gerado por um switch. Muito esclarecedor: http://741mhz.com/switch/

O compilador irá otimizá-lo de qualquer maneira – escolha a opção mais legível.

O Switch, se apenas por legibilidade. Gigante se as declarações forem mais difíceis de manter e mais difíceis de ler na minha opinião.

ERROR_01 : // fall-through intencional

ou

(ERROR_01 == numError) ||

O último é mais propenso a erros e requer mais digitação e formatação do que o primeiro.

Use switch, é para isso e o que os programadores esperam.

Eu colocaria os labels de maiúsculas e minúsculas redundantes – só para que as pessoas se sintam confortáveis, eu estava tentando lembrar quando / quais são as regras para deixá-las de fora.
Você não quer que o próximo programador que está trabalhando nele tenha que fazer qualquer pensamento desnecessário sobre os detalhes da linguagem (pode ser você daqui a alguns meses!)

Código para legibilidade. Se você quiser saber o que funciona melhor, use um criador de perfil, pois as otimizações e os compiladores variam, e os problemas de desempenho raramente estão onde as pessoas pensam que estão.

IMO, este é um exemplo perfeito de como o switch fall-through foi feito.

Compiladores são realmente bons em otimizar o switch . O gcc recente também é bom para otimizar várias condições em um if .

Eu fiz alguns casos de teste em godbolt .

Quando os valores de case são agrupados juntos, gcc, clang e icc são todos inteligentes o suficiente para usar um bitmap para verificar se um valor é um dos especiais.

eg gcc 5.2 -O3 compila o switch para (e if algo muito similar):

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

Observe que o bitmap é um dado imediato, portanto, não há um possível erro no cache de dados ao acessá-lo ou uma tabela de salto.

gcc 4.9.2 -O3 compila o switch para um bitmap, mas faz o 1U< com mov / shift. Compila a versão if para séries de ramificações.

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

Observe como ele subtrai 1 de errNumber (com lea para combinar essa operação com um movimento). Isso permite ajustar o bitmap em um 32bit imediato, evitando o movabsq imediato de movabsq que leva mais bytes de instrução.

Uma seqüência mais curta (em código de máquina) seria:

  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 

(A falha em usar jc fire_special_event é onipresente e é um erro do compilador .)

rep ret é usado em alvos de ramificação e seguindo ramificações condicionais, para o benefício do antigo AMD K8 e K10 (pré-Bulldozer): O que significa `rep ret`? . Sem isso, a previsão de ramificação não funciona tão bem nesses CPUs obsoletos.

bt (bit test) com um registrador arg é rápido. Ele combina o trabalho de deslocamento à esquerda de um 1 por bits errNumber e fazendo um test , mas ainda tem 1 ciclo de latência e apenas um único Intel uop. Ele é lento com um argumento de memory por causa de sua semântica CISC: com um operando de memory para a "cadeia de bits", o endereço do byte a ser testado é calculado com base no outro argumento (dividido por 8) e isn limitado ao pedaço de 1, 2, 4 ou 8 bytes apontado pelo operando memory.

Das tabelas de instruções da Agner Fog , uma instrução de mudança de contagem variável é mais lenta que a da Intel recente (2 uops em vez de 1, e shift não faz tudo o que é necessário).

Se os seus casos provavelmente permanecerem agrupados no futuro – se mais de um caso corresponder a um resultado – o switch poderá ser mais fácil de ler e manter.

Eles funcionam igualmente bem. O desempenho é o mesmo, dado um compilador moderno.

Eu prefiro se declarações sobre declarações de caso porque são mais legíveis e mais flexíveis – você pode adicionar outras condições não baseadas em igualdade numérica, como “|| max

switch é definitivamente preferido. É mais fácil olhar para a lista de casos de um switch e saber ao certo o que está fazendo do que ler a condição long if.

A duplicação na condição if é difícil para os olhos. Suponha que um dos == foi escrito != ; você notaria? Ou se uma instância de ‘numError’ foi escrita ‘nmuError’, que acabou de compilar?

Eu geralmente prefiro usar o polymorphism em vez do switch, mas sem mais detalhes do contexto, é difícil dizer.

Quanto ao desempenho, sua melhor aposta é usar um profiler para medir o desempenho de sua aplicação em condições semelhantes às que você espera na natureza. Caso contrário, você provavelmente está otimizando no lugar errado e no caminho errado.

Eu concordo com a compacidade da solução de switch, mas IMO você está seqüestrando a opção aqui.
O objective do switch é ter um tratamento diferente dependendo do valor.
Se você tivesse que explicar seu algoritmo no pseudo-código, você usaria um if porque, semanticamente, isso é o que é: if whatever_error faça isso
Então, a menos que você pretenda, algum dia, alterar seu código para ter um código específico para cada erro, eu usaria if .

Eu não tenho certeza sobre as melhores práticas, mas eu usaria switch – e, em seguida, interceptar fall-through intencional via ‘default’

Esteticamente, tenho a tendência de favorecer essa abordagem.

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

Torne os dados um pouco mais inteligentes para que possamos tornar a lógica um pouco mais burra.

Eu percebo que parece estranho. Aqui está a inspiração (de como eu faria em 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) 

Provavelmente, o primeiro é otimizado pelo compilador, o que explicaria porque o segundo loop é mais lento ao aumentar a contagem de loop.

Eu escolheria a declaração if por uma questão de clareza e convenção, embora tenha certeza de que alguns discordariam. Afinal, você está querendo fazer algo if alguma condição for verdadeira! Ter um interruptor com uma ação parece um pouco … desnecessário.

Por favor use o interruptor. A declaração if levará tempo proporcional ao número de condições.

Eu não sou a pessoa para lhe falar sobre velocidade e uso de memory, mas olhar para uma declaração de switch é muito mais fácil de entender então uma declaração if grande (especialmente 2-3 meses abaixo da linha)

Eu diria que use SWITCH. Desta forma, você só tem que implementar resultados diferentes. Seus dez casos idênticos podem usar o padrão. Se alguém mudar tudo o que precisa, é implementar explicitamente a mudança, sem necessidade de editar o padrão. Também é muito mais fácil adicionar ou remover casos de um SWITCH do que editar IF e ELSEIF.

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

Talvez até teste sua condição (neste caso, numerror) em relação a uma lista de possibilidades, uma matriz talvez para que seu SWITCH não seja usado, a menos que definitivamente haja um resultado.

Vendo que você só tem 30 códigos de erro, codifique sua própria tabela de saltos, então faça todas as opções de otimização (o salto será sempre mais rápido), ao invés de esperar que o compilador faça a coisa certa. Ele também torna o código muito pequeno (além da declaração estática da tabela de salto). Ele também tem o benefício de que, com um depurador, você pode modificar o comportamento em tempo de execução, caso precise, apenas digitando os dados da tabela diretamente.

Eu sei que é velho, mas

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

Variando a contagem de loop muda muito:

Enquanto if / else: 5ms Switch: 1ms Max Loops: 100000

Enquanto if / else: 5ms Switch: 3ms Max Loops: 1000000

Enquanto if / else: 5ms Switch: 14ms Max Loops: 10000000

Enquanto if / else: 5ms Switch: 149ms Max Loops: 100000000

(adicione mais instruções se quiser)

Quando se trata de compilar o programa, não sei se há alguma diferença. Mas quanto ao programa em si e manter o código o mais simples possível, eu pessoalmente acho que depende do que você quer fazer. if else if else declarações têm suas vantagens, que eu acho que são:

permite que você teste uma variável em relação a intervalos específicos, você pode usar funções (Biblioteca Padrão ou Pessoal) como condicionais.

(exemplo:

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

No entanto, senão se mais instruções podem ficar complicadas e confusas (apesar de suas melhores tentativas) com pressa. As declarações de troca tendem a ser mais claras, mais limpas e fáceis de ler; mas só pode ser usado para testar valores específicos (exemplo:

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

Eu prefiro if - else if - else declarações, mas realmente é com você. Se você quiser usar funções como condições, ou quiser testar algo em relação a um intervalo, matriz ou vetor e / ou não se importar em lidar com o aninhamento complicado, recomendo usar blocos Se mais if mais. Se você quiser testar valores únicos ou quiser um bloco limpo e fácil de ler, eu recomendo que você use blocos de casos switch ().