'Switch' é mais rápido que 'if'?


242

Uma switchdeclaração é realmente mais rápida que uma ifdeclaração?

Executei o código abaixo no compilador x64 C ++ do Visual Studio 2010 com o /Oxsinalizador:

#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 switchinstruções aparentemente usam tabelas de salto para otimizar a ramificação.

Questões:

  1. Como seria uma tabela básica de salto, em x86 ou x64?

  2. Esse código está usando uma tabela de salto?

  3. 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.


47
O que diabos as pessoas estão votando para fechar esse pensamento? Eles são tão fiéis à noção do compilador perfeitamente otimizador que qualquer pensamento de gerar código menos que o ideal é heresia? Será que a própria idéia de qualquer otimização qualquer lugar ofendê-los?
Crashworks

6
O que exatamente está errado com esta pergunta?
Tugrul Ates

25
Para quem quer saber o que há de errado com esta pergunta : Para iniciantes, não é uma pergunta, são três perguntas, o que significa que muitas das respostas agora abordam questões diferentes. Isso significa que será difícil aceitar qualquer resposta que responda a tudo . Além disso, a reação típica da pergunta acima é fechá-la como "não muito interessante" principalmente devido ao fato de que, nesse nível de otimização, você quase sempre está otimizando prematuramente . Por fim, 5196 vs. 5261 não deve ser suficiente para realmente se importar. Escreva o código lógico que faz sentido.
Lasse V. Karlsen

40
@Lasse: Você realmente preferia que eu publicasse três perguntas no SO? Também: 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?)
user541686

5
@ Robert: Bem, só tem mais de 20 comentários porque são meta-comentários. Existem apenas 7 comentários realmente relacionados à pergunta aqui. Opinião: não vejo como há "opinião" aqui. Há uma razão pela qual não estou vendo diferença de desempenho, não? É apenas gosto? Debate: Talvez, mas parece um tipo saudável de debate para mim, como eu já vi em outros lugares no SO (deixe-me saber se há algo contra isso). Argumentos: Não vejo nada de argumentativo aqui (a menos que você esteja tomando isso como sinônimo de 'debate'?). Discussão estendida: se você incluir esses meta-comentários.
user541686

Respostas:


122

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:

  1. Clang gera um que se parece com isso :

    test_switch(char):                       # @test_switch(char)
            movl    %edi, %eax
            cmpl    $19, %edi
            jbe     .LBB0_1
            retq
    .LBB0_1:
            jmpq    *.LJTI0_0(,%rax,8)
            jmp     void call<0u>()         # TAILCALL
            jmp     void call<1u>()         # TAILCALL
            jmp     void call<2u>()         # TAILCALL
            jmp     void call<3u>()         # TAILCALL
            jmp     void call<4u>()         # TAILCALL
            jmp     void call<5u>()         # TAILCALL
            jmp     void call<6u>()         # TAILCALL
            jmp     void call<7u>()         # TAILCALL
            jmp     void call<8u>()         # TAILCALL
            jmp     void call<9u>()         # TAILCALL
            jmp     void call<10u>()        # TAILCALL
            jmp     void call<11u>()        # TAILCALL
            jmp     void call<12u>()        # TAILCALL
            jmp     void call<13u>()        # TAILCALL
            jmp     void call<14u>()        # TAILCALL
            jmp     void call<15u>()        # TAILCALL
            jmp     void call<16u>()        # TAILCALL
            jmp     void call<17u>()        # TAILCALL
            jmp     void call<18u>()        # TAILCALL
            jmp     void call<19u>()        # TAILCALL
    .LJTI0_0:
            .quad   .LBB0_2
            .quad   .LBB0_3
            .quad   .LBB0_4
            .quad   .LBB0_5
            .quad   .LBB0_6
            .quad   .LBB0_7
            .quad   .LBB0_8
            .quad   .LBB0_9
            .quad   .LBB0_10
            .quad   .LBB0_11
            .quad   .LBB0_12
            .quad   .LBB0_13
            .quad   .LBB0_14
            .quad   .LBB0_15
            .quad   .LBB0_16
            .quad   .LBB0_17
            .quad   .LBB0_18
            .quad   .LBB0_19
            .quad   .LBB0_20
            .quad   .LBB0_21
  2. Posso dizer que não está usando uma tabela de salto - 4 instruções de comparação são claramente visíveis:

    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) 

    Uma solução baseada em tabela de salto não usa comparação.

  3. Ramos insuficientes para fazer com que o compilador gere uma tabela de salto ou seu compilador simplesmente não os gera. Não tenho certeza de qual.

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.


3
+1 para realmente responder à pergunta e para informações úteis. :-) No entanto, uma pergunta: Pelo que entendi, uma tabela de salto usa saltos indiretos ; isso está correto? Em caso afirmativo, isso geralmente não é mais lento devido à pré-busca / tubulação mais difícil?
user541686

1
@ Mehrdad: Sim, ele usa saltos indiretos. No entanto, um salto indireto (com a paralisação do oleoduto que acompanha) pode ser menor que centenas de saltos diretos. :)
Billy ONeal

1
@ Mehrdad: Não, infelizmente. :( Eu estou feliz que eu estou no acampamento de pessoas que sempre pensam que o IF é mais legível :)!
Billy ONeal

1
Poucas piadas - "[switches] só funciona quando a entrada pode ser delimitada de alguma forma" "precisa inserir alguma forma de teste if em torno da tabela para garantir que a entrada seja válida na tabela. Observe também que ela funciona apenas na área específica caso a entrada seja uma sequência de números consecutivos. ": é totalmente possível ter uma tabela pouco povoada, onde o ponteiro potencial seja lido e somente se não for NULL um salto, caso contrário, o caso padrão, se houver algum, então as switchsaídas. Soren disse várias outras coisas que eu queria dizer depois de ler esta resposta.
Tony Delroy

2
"Qualquer compilador que se preze vai ver uma escada if / else if e transformá-la em switch equivalente ou vice-versa" - algum suporte para essa afirmação? um compilador pode assumir que a ordem de suas ifcláusulas já foi ajustada manualmente para corresponder às necessidades de frequência e desempenho relativo, onde, switchtradicionalmente, é visto como um convite aberto para otimizar da maneira que o compilador escolher. Bom ponto de vista switch:-). O tamanho do código depende dos casos / intervalo - pode ser melhor. Finalmente, algumas enumerações, campos de bits e charcenários são inerentemente válidos / limitados e livres de sobrecarga.
Tony Delroy 25/07

47

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

00B14538  D8 09 AB 00 D8 09 AB 00 D8 09 AB 00 D8 09 AB 00  Ø.«.Ø.«.Ø.«.Ø.«.
00B14548  D8 09 AB 00 D8 09 AB 00 D8 09 AB 00 00 00 00 00  Ø.«.Ø.«.Ø.«.....
00B14558  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00B14568  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................

insira a descrição da imagem aqui

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


(edit: nvm, a resposta de Billy já tem o que eu estava sugerindo. Acho que é um bom complemento.) Seria bom incluir gcc -Ssaída: uma sequência de entradas .long L1/ .long L2tabela é 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).
Peter Cordes

31

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.


13
Eu não acho que isso responda à pergunta do OP. Em absoluto.
Billy ONeal

5
@Soren: Se essa fosse a "pergunta básica", eu não teria me incomodado com as 179 outras linhas da pergunta, teria sido apenas uma linha. :-)
user541686

8
@ Soren: Eu vejo pelo menos três sub-perguntas numeradas como parte da pergunta do OP. Você meramente anunciou a mesma resposta exata que se aplica a todas as questões de "desempenho" - a saber, que você precisa avaliar primeiro. Considere que talvez Mehrdad já tenha medido e isolado esse pedaço de código como um ponto de acesso. Nesses casos, sua resposta é pior do que inútil, é ruído.
Billy ONeal

2
Há uma linha borrada entre o que é uma tabela de salto e o que não depende da sua definição. I forneceram informações sobre sub-questão parte 3.
Soren

2
@noise: Se é a única resposta certa, nunca haveria um motivo para fazer qualquer pergunta sobre desempenho. No entanto, existem alguns de nós no mundo real que medem nosso software e, às vezes, não sabemos como tornar um pedaço de código mais rápido, uma vez medido. É óbvio que Mehrdad se esforçou nessa questão antes de fazer; e acho que suas perguntas específicas são mais do que respondíveis.
Billy ONeal

13

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:

  1. a diferença é muito pequena
  2. existe apenas um resultado, não uma série de resultados
  3. existem poucos casos

Meus resultados:

Eu adicionei:

printf("counter: %u\n", counter);

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 é:

switch (counter % 4 + 1)

no seu loop de switch, versus

const size_t c = counter % 4 + 1; 

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:

#include <stdlib.h>
#include <stdio.h>
#include <time.h>

#define MAX_COUNT (1 << 26)
size_t counter = 0;

long long testSwitch()
{
    clock_t start = clock();
    size_t i;
    for (i = 0; i < MAX_COUNT; i++)
    {
        const size_t c = rand() % 20 + 1;

        switch (c)
        {
                case 1: counter += 20; break;
                case 2: counter += 33; break;
                case 3: counter += 62; break;
                case 4: counter += 15; break;
                case 5: counter += 416; break;
                case 6: counter += 3545; break;
                case 7: counter += 23; break;
                case 8: counter += 81; break;
                case 9: counter += 256; break;
                case 10: counter += 15865; break;
                case 11: counter += 3234; break;
                case 12: counter += 22345; break;
                case 13: counter += 1242; break;
                case 14: counter += 12341; break;
                case 15: counter += 41; break;
                case 16: counter += 34321; break;
                case 17: counter += 232; break;
                case 18: counter += 144231; break;
                case 19: counter += 32; break;
                case 20: counter += 1231; break;
        }
    }
    return 1000 * (long long)(clock() - start) / CLOCKS_PER_SEC;
}

long long testIf()
{
    clock_t start = clock();
    size_t i;
    for (i = 0; i < MAX_COUNT; i++)
    {
        const size_t c = rand() % 20 + 1;
        if (c == 1) { counter += 20; }
        else if (c == 2) { counter += 33; }
        else if (c == 3) { counter += 62; }
        else if (c == 4) { counter += 15; }
        else if (c == 5) { counter += 416; }
        else if (c == 6) { counter += 3545; }
        else if (c == 7) { counter += 23; }
        else if (c == 8) { counter += 81; }
        else if (c == 9) { counter += 256; }
        else if (c == 10) { counter += 15865; }
        else if (c == 11) { counter += 3234; }
        else if (c == 12) { counter += 22345; }
        else if (c == 13) { counter += 1242; }
        else if (c == 14) { counter += 12341; }
        else if (c == 15) { counter += 41; }
        else if (c == 16) { counter += 34321; }
        else if (c == 17) { counter += 232; }
        else if (c == 18) { counter += 144231; }
        else if (c == 19) { counter += 32; }
        else if (c == 20) { counter += 1231; }
    }
    return 1000 * (long long)(clock() - start) / CLOCKS_PER_SEC;
}

int main()
{
    srand(time(NULL));
    printf("Starting...\n");
    printf("Switch statement: %lld ms\n", testSwitch()); fflush(stdout);
    printf("counter: %d\n", counter);
    counter = 0;
    srand(time(NULL));
    printf("If     statement: %lld ms\n", testIf()); fflush(stdout);
    printf("counter: %d\n", counter);
} 

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.


Idk, não posso provar; você obtém resultados diferentes?
user541686

+1: o benchmarking é difícil e você realmente não pode tirar conclusões de uma pequena diferença de tempo em uma única execução em um computador normal. Você pode tentar executar um grande número de testes e fazer algumas estatísticas sobre os resultados. Ou o processador de contagem executa ciclos de execução controlada em um emulador.
Thomas Padron-McCarthy

Er, onde exatamente você adicionar a printdeclaraçã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"?
user541686

1
@BobTurbo: 45983493 tem mais de 12 horas. Isso foi um erro de digitação?
Gus

1
Ótimo, agora eu tenho que ir fazê-lo novamente :)
BobTurbo

7

Um bom compilador de otimização, como o MSVC, pode gerar:

  1. uma tabela de salto simples se os estojos estiverem dispostos em um bom longo alcance
  2. uma tabela de salto esparsa (dois níveis) se houver muitas lacunas
  3. uma série de ifs se o número de casos for pequeno ou se os valores não estiverem próximos
  4. uma combinação acima se os casos representarem vários grupos de intervalos espaçados.

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.


Na verdade, um compilador também pode substituí-lo por um hash e jump, que tem um desempenho melhor do que a solução esparsa de dois níveis que você propõe.
Alice

5

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.


+1 obrigado pela resposta ao # 2! :) (Aliás, aqui estão os resultados com mais possibilidades.) #
User541686

4

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 .


1
Seus links quebraram.
TS

4

Aqui estão alguns resultados do antigo (agora difícil de encontrar) benchmark benchmark:

Test Name:   F000003                         Class Name:  Style
CPU Time:       0.781  nanoseconds           plus or minus     0.0715
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 2-way if/else if statement
 compare this test with F000004

Test Name:   F000004                         Class Name:  Style
CPU Time:        1.53  nanoseconds           plus or minus     0.0767
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 2-way switch statement
 compare this test with F000003

Test Name:   F000005                         Class Name:  Style
CPU Time:        7.70  nanoseconds           plus or minus      0.385
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 10-way if/else if statement
 compare this test with F000006

Test Name:   F000006                         Class Name:  Style
CPU Time:        2.00  nanoseconds           plus or minus     0.0999
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 10-way switch statement
 compare this test with F000005

Test Name:   F000007                         Class Name:  Style
CPU Time:        3.41  nanoseconds           plus or minus      0.171
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 10-way sparse switch statement
 compare this test with F000005 and F000006

O que podemos ver disso é que (nesta máquina, com este compilador - VC ++ 9.0 x64), cada ifteste 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 switchvs if/ 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 um if/ else, ~ 2,0 para switch).


1
É um pouco difícil saber o que fazer com isso, se não soubermos se o teste procura deliberadamente um valor que não corresponde ou apenas corresponde ao final da cadeia if/ elseversus espalhá-los etc. Não é possível encontrar as bench++fontes após 10 minutos pesquisando.
Tony Delroy

3

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


Isso é marcadamente falso; compiladores são mais do que capazes de fazer AMBAS dessas otimizações.
Alice

1
Alice, como um compilador deve saber quais casos ocorrerão com mais frequência do que outros casos nas cargas de trabalho esperadas? (A: Não é possível saber, por isso não pode possivelmente fazer uma otimização tal.)
Brian Kennedy

(1) pode ser feito facilmente, e é feito em alguns compiladores, simplesmente fazendo uma pesquisa binária. (2) pode ser previsto de várias maneiras, ou indicado ao compilador. Você nunca usou o "provável" ou o "improvável" do GCC?
Alice

E alguns compiladores permitem executar o programa em um modo que reúne estatísticas e otimiza a partir dessas informações.
Phil1970

2

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.


2

Não sei por que um é mais rápido e o outro é mais lento.

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 % 21versã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. :-)


2
Não vejo de onde pode vir centenas de vezes mais lento. O pior caso de uma ramificação imprevisível é uma paralisação de pipeline, que seria ~ 20 vezes mais lenta na maioria das CPUs modernas. Não centenas de vezes. (Ok, se você estiver usando um chip NetBurst idade pode ser 35x mais lento ...)
Billy ONeal

@ Billy: OK, então estou olhando um pouco à frente. Nos processadores Sandy Bridge , "Cada filial imprevisível liberará todo o pipeline, perdendo o trabalho de até cem instruções de vôo". Os oleodutos realmente começa mais profundo com cada geração, em geral ...
Nemo

1
Não é verdade. O P4 (NetBurst) teve 31 estágios de pipeline; Sandy Bridge tem significativamente menos etapas. Eu acho que a "perda do trabalho de mais ou menos 100 instruções" está no pressuposto de que o cache de instruções é invalidado. Para um salto indireto geral que de fato acontece, mas para algo como uma tabela de salto, é provável que o alvo do salto indireto esteja em algum lugar no cache de instruções.
Billy ONeal

@ Billy: Eu não acho que discordamos. Minha afirmação foi: "As agências imprevistas são dezenas a centenas de vezes mais caras que as agências previstas corretamente". Um leve exagero, talvez ... Mas há mais coisas acontecendo do que apenas acertos na profundidade do cache I e do pipeline de execução; pelo que li, a fila para decodificação sozinha é ~ 20 instruções.
Nemo

Se o hardware de predição de ramificação predizer o caminho de execução, os uops do caminho incorreto que estão no pipeline de instruções serão simplesmente removidos onde estão, sem interromper a execução. Não tenho idéia de como isso é possível (ou se estou interpretando mal), mas aparentemente não barracas de pipeline com filiais imprevisíveis em Nehalem? (Então, novamente, eu não tenho um i7; eu tenho um i5, então isso não se aplica ao meu caso.) #
User541686

1

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

counter += (4 - counter % 4);

parece-me a expressão de incremento correta que você deveria estar usando.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.