Uma switch
declaração é realmente mais rápida que uma if
declaração?
Executei o código abaixo no compilador x64 C ++ do Visual Studio 2010 com o /Ox
sinalizador:
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#define MAX_COUNT (1 << 29)
size_t counter = 0;
size_t testSwitch()
{
clock_t start = clock();
size_t i;
for (i = 0; i < MAX_COUNT; i++)
{
switch (counter % 4 + 1)
{
case 1: counter += 4; break;
case 2: counter += 3; break;
case 3: counter += 2; break;
case 4: counter += 1; break;
}
}
return 1000 * (clock() - start) / CLOCKS_PER_SEC;
}
size_t testIf()
{
clock_t start = clock();
size_t i;
for (i = 0; i < MAX_COUNT; i++)
{
const size_t c = counter % 4 + 1;
if (c == 1) { counter += 4; }
else if (c == 2) { counter += 3; }
else if (c == 3) { counter += 2; }
else if (c == 4) { counter += 1; }
}
return 1000 * (clock() - start) / CLOCKS_PER_SEC;
}
int main()
{
printf("Starting...\n");
printf("Switch statement: %u ms\n", testSwitch());
printf("If statement: %u ms\n", testIf());
}
e obtive estes resultados:
Comutador: 5261 ms
Se com: 5196 ms
Pelo que aprendi, as switch
instruções aparentemente usam tabelas de salto para otimizar a ramificação.
Questões:
Como seria uma tabela básica de salto, em x86 ou x64?
Esse código está usando uma tabela de salto?
Por que não há diferença de desempenho neste exemplo? Existe alguma situação em que não é uma diferença significativa de desempenho?
Desmontagem do código:
testIf:
13FE81B10 sub rsp,48h
13FE81B14 call qword ptr [__imp_clock (13FE81128h)]
13FE81B1A mov dword ptr [start],eax
13FE81B1E mov qword ptr [i],0
13FE81B27 jmp testIf+26h (13FE81B36h)
13FE81B29 mov rax,qword ptr [i]
13FE81B2E inc rax
13FE81B31 mov qword ptr [i],rax
13FE81B36 cmp qword ptr [i],20000000h
13FE81B3F jae testIf+0C3h (13FE81BD3h)
13FE81B45 xor edx,edx
13FE81B47 mov rax,qword ptr [counter (13FE835D0h)]
13FE81B4E mov ecx,4
13FE81B53 div rax,rcx
13FE81B56 mov rax,rdx
13FE81B59 inc rax
13FE81B5C mov qword ptr [c],rax
13FE81B61 cmp qword ptr [c],1
13FE81B67 jne testIf+6Dh (13FE81B7Dh)
13FE81B69 mov rax,qword ptr [counter (13FE835D0h)]
13FE81B70 add rax,4
13FE81B74 mov qword ptr [counter (13FE835D0h)],rax
13FE81B7B jmp testIf+0BEh (13FE81BCEh)
13FE81B7D cmp qword ptr [c],2
13FE81B83 jne testIf+89h (13FE81B99h)
13FE81B85 mov rax,qword ptr [counter (13FE835D0h)]
13FE81B8C add rax,3
13FE81B90 mov qword ptr [counter (13FE835D0h)],rax
13FE81B97 jmp testIf+0BEh (13FE81BCEh)
13FE81B99 cmp qword ptr [c],3
13FE81B9F jne testIf+0A5h (13FE81BB5h)
13FE81BA1 mov rax,qword ptr [counter (13FE835D0h)]
13FE81BA8 add rax,2
13FE81BAC mov qword ptr [counter (13FE835D0h)],rax
13FE81BB3 jmp testIf+0BEh (13FE81BCEh)
13FE81BB5 cmp qword ptr [c],4
13FE81BBB jne testIf+0BEh (13FE81BCEh)
13FE81BBD mov rax,qword ptr [counter (13FE835D0h)]
13FE81BC4 inc rax
13FE81BC7 mov qword ptr [counter (13FE835D0h)],rax
13FE81BCE jmp testIf+19h (13FE81B29h)
13FE81BD3 call qword ptr [__imp_clock (13FE81128h)]
13FE81BD9 sub eax,dword ptr [start]
13FE81BDD imul eax,eax,3E8h
13FE81BE3 cdq
13FE81BE4 mov ecx,3E8h
13FE81BE9 idiv eax,ecx
13FE81BEB cdqe
13FE81BED add rsp,48h
13FE81BF1 ret
testSwitch:
13FE81C00 sub rsp,48h
13FE81C04 call qword ptr [__imp_clock (13FE81128h)]
13FE81C0A mov dword ptr [start],eax
13FE81C0E mov qword ptr [i],0
13FE81C17 jmp testSwitch+26h (13FE81C26h)
13FE81C19 mov rax,qword ptr [i]
13FE81C1E inc rax
13FE81C21 mov qword ptr [i],rax
13FE81C26 cmp qword ptr [i],20000000h
13FE81C2F jae testSwitch+0C5h (13FE81CC5h)
13FE81C35 xor edx,edx
13FE81C37 mov rax,qword ptr [counter (13FE835D0h)]
13FE81C3E mov ecx,4
13FE81C43 div rax,rcx
13FE81C46 mov rax,rdx
13FE81C49 inc rax
13FE81C4C mov qword ptr [rsp+30h],rax
13FE81C51 cmp qword ptr [rsp+30h],1
13FE81C57 je testSwitch+73h (13FE81C73h)
13FE81C59 cmp qword ptr [rsp+30h],2
13FE81C5F je testSwitch+87h (13FE81C87h)
13FE81C61 cmp qword ptr [rsp+30h],3
13FE81C67 je testSwitch+9Bh (13FE81C9Bh)
13FE81C69 cmp qword ptr [rsp+30h],4
13FE81C6F je testSwitch+0AFh (13FE81CAFh)
13FE81C71 jmp testSwitch+0C0h (13FE81CC0h)
13FE81C73 mov rax,qword ptr [counter (13FE835D0h)]
13FE81C7A add rax,4
13FE81C7E mov qword ptr [counter (13FE835D0h)],rax
13FE81C85 jmp testSwitch+0C0h (13FE81CC0h)
13FE81C87 mov rax,qword ptr [counter (13FE835D0h)]
13FE81C8E add rax,3
13FE81C92 mov qword ptr [counter (13FE835D0h)],rax
13FE81C99 jmp testSwitch+0C0h (13FE81CC0h)
13FE81C9B mov rax,qword ptr [counter (13FE835D0h)]
13FE81CA2 add rax,2
13FE81CA6 mov qword ptr [counter (13FE835D0h)],rax
13FE81CAD jmp testSwitch+0C0h (13FE81CC0h)
13FE81CAF mov rax,qword ptr [counter (13FE835D0h)]
13FE81CB6 inc rax
13FE81CB9 mov qword ptr [counter (13FE835D0h)],rax
13FE81CC0 jmp testSwitch+19h (13FE81C19h)
13FE81CC5 call qword ptr [__imp_clock (13FE81128h)]
13FE81CCB sub eax,dword ptr [start]
13FE81CCF imul eax,eax,3E8h
13FE81CD5 cdq
13FE81CD6 mov ecx,3E8h
13FE81CDB idiv eax,ecx
13FE81CDD cdqe
13FE81CDF add rsp,48h
13FE81CE3 ret
Atualizar:
Resultados interessantes aqui . Não sei por que um é mais rápido e o outro é mais lento.
c
performance
switch-statement
assembly
jump-table
user541686
fonte
fonte
5196 vs. 5261 shouldn't be enough to actually care
-> Não tenho certeza se você entendeu mal a pergunta ou se eu entendi mal o seu comentário, mas não é o ponto principal da minha pergunta perguntar por que não há diferença? (Eu já afirmar que é uma diferença significativa para se preocupar?)Respostas:
Existem várias otimizações que um compilador pode fazer em um switch. Eu não acho que a "tabela de atalhos" mencionada seja muito útil, pois só funciona quando a entrada pode ser delimitada de alguma forma.
C Pseudocódigo para uma "tabela de salto" seria algo parecido com isto - observe que, na prática, o compilador precisaria inserir alguma forma de teste if ao redor da tabela para garantir que a entrada fosse válida na tabela. Observe também que só funciona no caso específico em que a entrada é uma sequência de números consecutivos.
Se o número de ramificações em um comutador for extremamente grande, um compilador pode fazer coisas como usar a pesquisa binária nos valores do comutador, o que (na minha opinião) seria uma otimização muito mais útil, pois aumenta significativamente o desempenho em alguns cenários, é tão geral quanto uma opção e não resulta em maior tamanho de código gerado. Mas, para ver isso, seu código de teste precisaria de muito mais ramificações para ver qualquer diferença.
Para responder suas perguntas específicas:
Clang gera um que se parece com isso :
Posso dizer que não está usando uma tabela de salto - 4 instruções de comparação são claramente visíveis:
Uma solução baseada em tabela de salto não usa comparação.
EDIT 2014 : Houve alguma discussão em outros lugares por pessoas familiarizadas com o otimizador LLVM dizendo que a otimização da tabela de salto pode ser importante em muitos cenários; por exemplo, nos casos em que há uma enumeração com muitos valores e muitos casos contra valores na referida enumeração. Dito isso, eu mantenho o que disse acima em 2011 - muitas vezes vejo pessoas pensando "se eu mudar, será o mesmo tempo, não importa quantos casos eu tenha" - e isso é completamente falso. Mesmo com uma tabela de salto, você obtém o custo indireto do salto e paga pelas entradas da tabela para cada caso; e a largura de banda da memória é um grande problema no hardware moderno.
Escreva código para facilitar a leitura. Qualquer compilador que se preze verá uma escada if / else if e a transformará em switch equivalente ou vice-versa, se for mais rápido fazê-lo.
fonte
switch
saídas. Soren disse várias outras coisas que eu queria dizer depois de ler esta resposta.if
cláusulas já foi ajustada manualmente para corresponder às necessidades de frequência e desempenho relativo, onde,switch
tradicionalmente, é visto como um convite aberto para otimizar da maneira que o compilador escolher. Bom ponto de vistaswitch
:-). O tamanho do código depende dos casos / intervalo - pode ser melhor. Finalmente, algumas enumerações, campos de bits echar
cenários são inerentemente válidos / limitados e livres de sobrecarga.Para sua pergunta:
1. Como seria uma tabela básica de salto, em x86 ou x64?
A tabela de salto é o endereço de memória que mantém o ponteiro para os rótulos em algo como a estrutura da matriz. O exemplo a seguir ajudará você a entender como as tabelas de salto são dispostas
Onde 00B14538 é o ponteiro para a tabela Jump e um valor como D8 09 AB 00 representa o ponteiro do rótulo.
2. Esse código está usando uma tabela de salto? Não neste caso.
3.Por que não há diferença de desempenho neste exemplo?
Não há diferença de desempenho porque as instruções para ambos os casos parecem iguais, sem tabela de salto.
4. Existe alguma situação em que haja uma diferença significativa de desempenho?
Se você tem muito longa sequência de se verificar, nesse caso usando uma tabela de salto melhora de desempenho (ramificação / instruções JMP são caros , se eles não prever quase perfeitamente), mas vem com o custo de memória.
O código para todas as instruções de comparação também possui algum tamanho, portanto, especialmente com ponteiros ou deslocamentos de 32 bits, uma consulta na tabela de salto único pode não custar muito mais tamanho em um executável.
Conclusão: O compilador é inteligente o suficiente para lidar com esse caso e gerar instruções apropriadas :)
fonte
gcc -S
saída: uma sequência de entradas.long L1
/.long L2
tabela é mais significativa que um hexdump e mais útil para alguém que quer aprender a olhar para o compilador. (Embora eu ache que você deveria apenas olhar o código do switch para ver se era um jmp indireto ou um monte de jcc).O compilador é livre para compilar a instrução switch como um código equivalente à instrução if ou para criar uma tabela de salto. Provavelmente, ele escolherá um do outro com base no que será executado mais rapidamente ou gerará o menor código, dependendo do que você especificou nas opções do compilador - portanto, na pior das hipóteses, será a mesma velocidade das instruções if
Eu confiaria no compilador para fazer a melhor escolha e focar no que torna o código mais legível.
Se o número de casos se tornar muito grande, uma tabela de salto será muito mais rápida que uma série de if. No entanto, se as etapas entre os valores forem muito grandes, a tabela de salto poderá se tornar grande e o compilador poderá optar por não gerar uma.
fonte
Como você sabe que seu computador não estava executando alguma tarefa não relacionada ao teste durante o loop de teste do switch e executando menos tarefas durante o loop de teste if? Os resultados do seu teste não mostram nada como:
Meus resultados:
Eu adicionei:
até o fim para que ele não otimize o loop, pois o contador nunca foi usado no seu exemplo. Por que o compilador executaria o loop? Imediatamente, a troca estava sempre ganhando, mesmo com uma micro-referência.
O outro problema com o seu código é:
no seu loop de switch, versus
no seu loop se. Diferença muito grande se você consertar isso. Acredito que colocar a instrução dentro da instrução switch faz com que o compilador envie o valor diretamente para os registradores da CPU, em vez de colocá-lo na pilha primeiro. Portanto, isso é a favor da instrução switch e não um teste equilibrado.
Ah, e acho que você também deve redefinir o contador entre os testes. De fato, você provavelmente deve usar algum tipo de número aleatório em vez de +1, +2, +3 etc, pois provavelmente otimizará algo lá. Por número aleatório, quero dizer um número com base no horário atual, por exemplo. Caso contrário, o compilador pode transformar ambas as suas funções em uma operação matemática longa e nem mesmo se preocupar com loops.
Modifiquei o código de Ryan apenas o suficiente para garantir que o compilador não conseguisse entender as coisas antes da execução do código:
interruptor: 3740
se: 3980
(resultados semelhantes em várias tentativas)
Também reduzi o número de casos / ifs para 5 e a função de comutação ainda venceu.
fonte
print
declaração? Adicionei no final de todo o programa e não vi diferença. Eu também não entendo qual é o "problema" com o outro ... mente explicando qual é a "grande diferença"?Um bom compilador de otimização, como o MSVC, pode gerar:
Em resumo, se o switch parecer mais lento que uma série de ifs, o compilador poderá apenas convertê-lo em um. E é provável que não seja apenas uma sequência de comparações para cada caso, mas uma árvore de pesquisa binária. Veja aqui um exemplo.
fonte
Vou responder 2) e fazer alguns comentários gerais. 2) Não, não há tabela de salto no código de montagem que você postou. Uma tabela de salto é uma tabela de destinos de salto e uma ou duas instruções para saltar diretamente para um local indexado da tabela. Uma tabela de salto faria mais sentido quando houver muitos destinos possíveis de troca. Talvez o otimizador saiba que a lógica simples se senão é mais rápida, a menos que o número de destinos seja maior que algum limite. Tente seu exemplo novamente com digamos 20 possibilidades em vez de 4.
fonte
Fiquei intrigado e dei uma olhada no que eu poderia mudar no seu exemplo para que ele executasse a instrução switch mais rapidamente.
Se você chegar a 40 instruções if e adicionar um caso 0, o bloco if será executado mais lentamente que a instrução switch equivalente. Tenho os resultados aqui: https://www.ideone.com/KZeCz .
O efeito da remoção do caso 0 pode ser visto aqui: https://www.ideone.com/LFnrX .
fonte
Aqui estão alguns resultados do antigo (agora difícil de encontrar) benchmark benchmark:
O que podemos ver disso é que (nesta máquina, com este compilador - VC ++ 9.0 x64), cada
if
teste leva cerca de 0,7 nanossegundos. À medida que o número de testes aumenta, o tempo varia quase perfeitamente linearmente.Com a instrução switch, quase não há diferença de velocidade entre um teste de 2 e 10 vias, desde que os valores sejam densos. O teste de 10 vias com valores esparsos leva cerca de 1,6x mais tempo que o teste de 10 vias com valores densos - mas mesmo com valores esparsos, ainda melhor do que o dobro da velocidade de um sistema de 10 vias
if
/else if
.Conclusão: usar apenas um teste de quatro direções não mostra muito sobre o desempenho de
switch
vsif
/else
. Se você olhar para os números desse código, é muito fácil interpolar o fato de que, para um teste de quatro vias, esperaríamos que os dois produzissem resultados bastante semelhantes (~ 2,8 nanossegundos para umif
/else
, ~ 2,0 paraswitch
).fonte
if
/else
versus espalhá-los etc. Não é possível encontrar asbench++
fontes após 10 minutos pesquisando.Observe que quando um switch NÃO é compilado em uma tabela de salto, é possível escrever muitas vezes se é mais eficiente do que o switch ...
(1) se os casos tiverem uma ordem, em vez do pior teste para todos os N, você poderá escrever seus if's para testar se estão na metade superior ou inferior, em cada metade desse estilo de pesquisa binária ... resultando em o pior caso é logN em vez de N
(2) se determinados casos / grupos são muito mais frequentes do que outros casos, então projetar seus if's para isolar esses casos primeiro pode acelerar o tempo médio
fonte
Não, esses são if then jump else se then jump else ... Uma tabela de salto teria uma tabela de endereços ou usaria um hash ou algo assim.
Mais rápido ou mais lento é subjetivo. Você pode, por exemplo, fazer com que o caso 1 seja a última coisa, em vez de primeiro, e se o seu programa de teste ou mundo real usasse o caso 1 na maioria das vezes, o código seria mais lento com essa implementação. Portanto, apenas reorganizar a lista de casos, dependendo da implementação, pode fazer uma grande diferença.
Se você usou os casos 0-3 em vez de 1-4, o compilador pode ter usado uma tabela de salto, o compilador deve ter descoberto a remoção do seu +1 de qualquer maneira. Talvez tenha sido o pequeno número de itens. Se você fez 0 - 15 ou 0 - 31, por exemplo, pode ter implementado com uma tabela ou usado algum outro atalho. O compilador é livre para escolher como implementa as coisas, desde que atenda à funcionalidade do código-fonte. E isso entra nas diferenças do compilador e nas diferenças de versão e de otimização. Se você deseja uma tabela de salto, faça uma tabela de salto, se desejar uma árvore se-então-outro, faça uma árvore se-então-outro. Se você deseja que o compilador decida, use uma instrução switch / case.
fonte
Na verdade, isso não é muito difícil de explicar ... Se você se lembrar que os galhos imprevisíveis são dezenas a centenas de vezes mais caros do que os galhos previstos corretamente.
No
% 20
versão, o primeiro caso / se é sempre o que ocorre. As CPUs modernas "aprendem" quais ramificações geralmente são obtidas e quais não são, para que possam prever facilmente como esse ramo se comportará em quase todas as iterações do loop. Isso explica por que a versão "se" voa; ele nunca precisa executar nada após o primeiro teste e (corretamente) prevê o resultado desse teste para a maioria das iterações. Obviamente, o "switch" é implementado de maneira um pouco diferente - talvez até uma tabela de salto, que pode ser lenta graças ao ramo computado.Na
% 21
versão, os ramos são essencialmente aleatórios. Portanto, não apenas muitos deles executam todas as iterações, como a CPU não consegue adivinhar para que lado seguirá. É o caso em que uma tabela de salto (ou outra otimização "switch") provavelmente ajudará.É muito difícil prever como um pedaço de código será executado com um compilador e uma CPU modernos, e fica mais difícil a cada geração. O melhor conselho é "nem se preocupe em tentar; sempre faça um perfil". Esse conselho fica melhor - e o conjunto de pessoas que podem ignorá-lo com sucesso fica menor - a cada ano.
Tudo isso para dizer que minha explicação acima é em grande parte um palpite. :-)
fonte
Nenhum. Na maioria dos casos em que você entra no montador e faz medições reais de desempenho, sua pergunta é simplesmente errada. Para o exemplo dado, seu pensamento é definitivamente muito curto, pois
parece-me a expressão de incremento correta que você deveria estar usando.
fonte