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();
}
Foi útil?

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

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top