Vantagem de mudar a instrução if-else
-
01-07-2019 - |
Pergunta
Qual é a melhor prática para usar um switch
declaração versus usar um if
declaração para 30 unsigned
enumerações onde cerca de 10 têm uma ação esperada (que atualmente é a mesma ação).O desempenho e o 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
declaração:
// 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();
}
Solução
Use o switch.
Na pior das hipóteses, o compilador 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 em primeiro lugar na declaração de interruptor.
Na melhor das hipóteses, o otimizador pode encontrar uma maneira melhor de gerar o código. Coisas comuns que um compilador faz é construir uma árvore de decisão binária (o SAVES compara e salta no caso médio) ou simplesmente construir uma tabela de salto (funciona sem comparação).
Outras dicas
Para o caso especial que você forneceu no 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 de como resolvê -lo. Você pode usar o STD :: Set, por exemplo:
bool RequiresSpecialEvent(int numError)
{
return specialSet.find(numError) != specialSet.end();
}
Não estou sugerindo que esta seja a melhor implementação do requerSpecialEvent, apenas que é uma opção. Você ainda pode usar uma cadeia de switch ou if-else, ou uma tabela de pesquisa ou alguma manipulação de bits no valor, qualquer que seja. Quanto mais obscuro seu processo de decisão se torna, mais valor você derivará de tê -lo em uma função isolada.
O interruptor é mais rápido.
Apenas tente if/else-30 valores diferentes dentro de um loop e compare-os com o mesmo código usando 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 compilação os valores dentro de cada caso.Isso significa que o seguinte código:
// 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 declararão e definirão variáveis constantes na mesma unidade de compilação.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” vs."acoplamento de código".
(Não que um switch não possa ser escrito para ser confuso como o inferno...A maioria das opções que vejo atualmente são desta categoria "confusa""...Mas essa é outra história...)
Editar 21/09/2008:
bk1e adicionou o seguinte comentário:"Definir constantes como enums em um arquivo de cabeçalho é outra maneira de lidar com isso".
Claro que é.
O objetivo de um tipo externo era dissociar o valor da fonte.Definir esse valor como uma macro, como uma simples declaração const int ou mesmo como um enum tem o efeito colateral de incorporar o valor.Portanto, caso o valor define, o valor enum ou o valor const int mudem, será necessária uma recompilação.A declaração extern significa que não há necessidade de recompilar em caso de alteração de valor, mas por outro lado, impossibilita o uso do switch.Sendo a conclusão Usar switch aumentará o acoplamento entre o código do switch e as variáveis usadas como casos.Quando estiver ok, use o switch.Quando não é, então, não é surpresa.
.
Editar 15/01/2013:
Vlad Lazarenko comentou minha resposta, fornecendo um link para seu estudo aprofundado do código assembly gerado por um switch.Muito esclarecedor: http://741mhz.com/switch/
O compilador irá otimizá-lo de qualquer maneira - escolha a opção, pois é a mais legível.
O Switch, mesmo que apenas para facilitar a leitura.As declarações if gigantes são mais difíceis de manter e mais difíceis de ler, na minha opinião.
ERRO_01 :// queda intencional
ou
(ERROR_01 == numError) ||
O último é mais sujeito a erros e requer mais digitação e formatação do que o primeiro.
Código para legibilidade.Se você quiser saber o que tem melhor desempenho, 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.
Use switch, é para isso que serve e o que os programadores esperam.
Eu colocaria rótulos de casos redundantes - apenas para fazer as pessoas se sentirem confortáveis, eu estava tentando lembrar quando/quais são as regras para deixá-los de fora.
Você não quer que o próximo programador trabalhando nisso tenha que pensar desnecessariamente sobre os detalhes da linguagem (pode ser você daqui a alguns meses!)
Os compiladores são realmente bons em otimizar switch
.O gcc recente também é bom para otimizar várias condições em um if
.
Eu fiz alguns casos de teste em raio divino.
Quando o case
os valores são agrupados juntos, gcc, clang e icc são inteligentes o suficiente para usar um bitmap para verificar se um valor é um dos especiais.
por exemplo.gcc 5.2 -O3 compila o switch
para (e o if
algo muito semelhante):
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 são dados imediatos, portanto não há nenhuma falha potencial no cache de dados ao acessá-lo ou em uma tabela de salto.
gcc 4.9.2 -O3 compila o switch
para um bitmap, mas o 1U<<errNumber
com movimento/shift.Ele compila o if
versão para série 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 imediato de 32 bits, evitando o imediato de 64 bits. movabsq
que ocupa mais bytes de instrução.
Uma sequê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 não utilização jc fire_special_event
é onipresente e é um bug do compilador.)
rep ret
é usado em alvos de ramificação e seguintes ramificações condicionais, para o benefício dos antigos AMD K8 e K10 (pré-Bulldozer): O que significa `rep ret`?.Sem ele, a previsão de ramificação não funciona tão bem nessas CPUs obsoletas.
bt
(teste de bit) com um registro arg é rápido.Ele combina o trabalho de deslocar 1 para a esquerda por errNumber
pedaços e fazendo um test
, mas ainda tem latência de 1 ciclo e apenas um único Intel Uop.É lento com um argumento de memória por causa de sua semântica muito CISC:com um operando de memória para a "string de bits", o endereço do byte a ser testado é calculado com base no outro argumento (dividido por 8) e não está limitado ao pedaço de 1, 2, 4 ou 8 bytes apontado para pelo operando memória.
De Tabelas de instruções de Agner Fog, uma instrução de deslocamento de contagem variável é mais lenta que uma bt
na Intel recente (2 uops em vez de 1, e o shift não faz tudo o que é necessário).
IMO, este é um exemplo perfeito do motivo pelo qual o switch fall-through foi criado.
Se for provável que seus casos permaneçam agrupados no futuro - se mais de um caso corresponder a um resultado - a opção poderá ser mais fácil de ler e manter.
Eles funcionam igualmente bem.O desempenho é praticamente o mesmo em um compilador moderno.
Eu prefiro instruções if em vez de instruções case porque elas são mais legíveis e mais flexíveis - você pode adicionar outras condições não baseadas em igualdade numérica, como " || max < min ".Mas para o caso simples que você postou aqui, isso realmente não importa, apenas faça o que for mais legível para você.
switch é definitivamente preferido.É mais fácil olhar a lista de casos de um switch e saber com certeza o que ele está fazendo do que ler a longa condição if.
A duplicação no if
a condição é difícil para os olhos.Suponha que um dos ==
foi escrito !=
;você notaria?Ou se uma instância de 'numError' foi escrita 'nmuError', o que aconteceu para compilar?
Geralmente prefiro usar o polimorfismo em vez da opção, mas sem mais detalhes do contexto, é difícil dizer.
Quanto ao desempenho, sua melhor aposta é usar um criador de perfil para medir o desempenho do seu aplicativo em condições semelhantes às que você espera na natureza.Caso contrário, você provavelmente estará otimizando no lugar errado e da maneira errada.
Eu concordo com a compacidade da solução switch, mas IMO você está sequestrando o interruptor aqui.
O objetivo do switch é ter diferente manuseio dependendo do valor.
Se você tivesse que explicar seu algo em pseudocódigo, você usaria um if porque, semanticamente, é isso: se seja o que for_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 se.
Não tenho certeza sobre as práticas recomendadas, mas usaria switch - e, em seguida, capturaria falhas intencionais via 'padrão'
Esteticamente tenho tendência a favorecer esta 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 isso 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, isso explicaria porque o segundo loop é mais lento ao aumentar a contagem de loops.
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 é verdadeira!Ter um switch com uma ação parece um pouco...desnecessário.
Não sou a pessoa certa para falar sobre velocidade e uso de memória, mas olhar para uma instrução switch é muito mais fácil de entender do que uma instrução if grande (especialmente 2 a 3 meses depois)
Eu diria que use SWITCH.Dessa forma, você só precisa implementar resultados diferentes.Seus dez casos idênticos podem usar o padrão.Se houver uma alteração, tudo o que você precisa é implementar explicitamente a alteração, não há 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, talvez um array para que seu SWITCH nem seja usado, a menos que definitivamente haja um resultado.
Visto que você tem apenas 30 códigos de erro, codifique sua própria tabela de salto e faça você mesmo todas as escolhas de otimização (o salto sempre será mais rápido), em vez de esperar que o compilador faça a coisa certa.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 colateral de que, com um depurador, você pode modificar o comportamento em tempo de execução, se necessário, apenas cutucando os dados da tabela diretamente.
Eu sei que é antigo, 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);
}
}
Variar a contagem de loops muda muito:
Enquanto if/else:Switch de 5ms:1ms Max Loops:100.000
Enquanto if/else:Switch de 5ms:Loops máximos de 3ms:1.000.000
Enquanto if/else:Switch de 5ms:14ms Max Loops:10000000
Enquanto if/else:Switch de 5ms:149ms Max Loops:100000000
(adicione mais declarações se quiser)
Na hora de compilar o programa, não sei se há alguma diferença.Mas quanto ao programa em si e a manter o código o mais simples possível, pessoalmente acho que depende do que você deseja fazer.if else if else as declarações têm suas vantagens, que eu acho que são:
Permita que você teste uma variável em faixas específicas, 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, as instruções If else if else podem ficar complicadas e confusas (apesar de suas melhores tentativas) rapidamente.As declarações de mudança tendem a ser mais claras, 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: "<<a<<"\n";
break;
//other case statements
default:
cout<<"a is not between the range or is not a good value\n"
break;
Eu prefiro declarações if - else if - else, mas isso realmente depende de você.Se você quiser usar funções como condições, ou quiser testar algo em um intervalo, array ou vetor e/ou não se importar em lidar com o aninhamento complicado, eu recomendaria usar os blocos If else if else.Se você quiser testar valores únicos ou quiser um bloco limpo e fácil de ler, eu recomendo que você use blocos case switch().