Velocidade If vs. Switch

112

As instruções switch são geralmente mais rápidas do que as instruções if-else-if equivalentes (como, por exemplo, descrito neste artigo ) devido às otimizações do compilador.

Como essa otimização realmente funciona? Alguém tem uma boa explicação?

Dirk Vollmar
fonte
Uma possível boa resposta: dotnetperls.com/if-switch-performance
Babak

Respostas:

185

O compilador pode construir tabelas de salto onde aplicável. Por exemplo, ao usar o refletor para examinar o código produzido, você verá que, para grandes interruptores em strings, o compilador irá gerar um código que usa uma tabela hash para despachá-los. A tabela de hash usa as strings como chaves e delega os casecódigos como valores.

Isso tem melhor tempo de execução assintótico do que muitos iftestes encadeados e é realmente mais rápido mesmo para relativamente poucas strings.

Konrad Rudolph
fonte
6
Boa resposta, interessante sobre a mesa de hash.
BobbyShaftoe
4
Eles também convertem em comparações de árvore em alguns casos. O raciocínio é um tanto complexo, mas basicamente se resume à indireção da tabela que neutraliza os buffers de destino de salto de CPU modernos e, assim, apaga o preditor de ramificação. Lembro-me vagamente de um artigo em uma conferência do GCC sobre codegen para switches.
olliej
Isso significa: switch (a) case "x": case "y": case "z": // algo quebre; } é mais rápido do que: if (a == "x" || a == "b" || a == "c") // algo certo?
yazanpro
aqui não temos aninhado if else, apenas OR então o que você acha?
yazanpro
@yazanpro Em compiladores antigos, potencialmente sim (mas note que o número de casos é tão pequeno que pode não fazer diferença!). No entanto, os compiladores modernos fazem muito mais análises de código. Como consequência, eles podem descobrir que esses dois trechos de código são equivalentes e aplicar as mesmas otimizações. Mas isso é pura especulação da minha parte, não sei se algum compilador realmente faz isso.
Konrad Rudolph
15

Esta é uma pequena simplificação, pois normalmente qualquer compilador moderno que encontra uma if..else if ..sequência que poderia ser convertida trivialmente em uma instrução switch por uma pessoa, o compilador também o fará. Mas apenas para adicionar diversão extra, o compilador não é restrito pela sintaxe, portanto, pode gerar internamente "switch" como declarações que possuem uma mistura de intervalos, alvos únicos, etc - e eles podem (e fazem) fazer isso para switch e if. .outras declarações.

De qualquer forma, uma extensão da resposta de Konrad é que o compilador pode gerar uma tabela de salto, mas isso não é necessariamente garantido (nem desejável). Por uma variedade de razões, as tabelas de salto fazem coisas ruins para os preditores de ramificação nos processadores modernos, e as próprias tabelas fazem coisas ruins para o comportamento do cache, por exemplo.

switch(a) { case 0: ...; break; case 1: ...; break; }

Se um compilador realmente gerasse uma tabela de salto para isso, provavelmente seria mais lento que o if..else if..código de estilo alternativo por causa da tabela de salto derrotando a previsão de ramificação.

olliej
fonte
4

As estatísticas de no-match podem não ser boas.

Se você realmente fizer o download da fonte, os valores sem correspondência são conhecidos como 21, em ambos os casos, if e switch. Um compilador deve ser capaz de abstrair, sabendo qual instrução deve ser executada o tempo todo, e uma CPU deve ser capaz de fazer previsões de ramificações corretamente.

O caso mais interessante é quando nem todos os casos quebram, em minha opinião, mas esse pode não ter sido o escopo do experimento.

Calyth
fonte
4

As instruções switch / case podem ser tipicamente mais rápidas de nível 1, mas quando você começa a usar 2 ou mais, as instruções switch / case levam 2 a 3 vezes mais tempo do que as instruções if / else aninhadas.

Este artigo apresenta algumas comparações de velocidade destacando as diferenças de velocidade quando tais instruções são aninhadas.

Por exemplo, de acordo com seus testes, código de amostra como o seguinte:

if (x % 3 == 0)
            if (y % 3 == 0)
                total += 3;
            else if (y % 3 == 1)
                total += 2;
            else if (y % 3 == 2)
                total += 1;
            else
                total += 0;
        else if (x % 3 == 1)
            if (y % 3 == 0)
                total += 3;
            else if (y % 3 == 1)
                total += 2;
            else if (y % 3 == 2)
                total += 1;
            else
                total += 0;
        else if (x % 3 == 2)
            if (y % 3 == 0)
                total += 3;
            else if (y % 3 == 1)
                total += 2;
            else if (y % 3 == 2)
                total += 1;
            else
                total += 0;
        else
            if (y % 3 == 0)
                total += 3;
            else if (y % 3 == 1)
                total += 2;
            else if (y % 3 == 2)
                total += 1;
            else
                total += 0;

terminou na metade do tempo que a instrução switch / case equivalente levou para ser executada:

switch (x % 3)
    {
        case 0:
            switch (y % 3)
            {
                case 0: total += 3;
                    break;
                case 1: total += 2;
                    break;
                case 2: total += 1;
                    break;
                default: total += 0;
                    break;
            }
            break;
        case 1:
            switch (y % 3)
            {
                case 0: total += 3;
                    break;
                case 1: total += 2;
                    break;
                case 2: total += 1;
                    break;
                default: total += 0;
                    break;
            }
            break;
    case 2:
            switch (y % 3)
            {
                case 0: total += 3;
                    break;
                case 1: total += 2;
                    break;
                case 2: total += 1;
                    break;
                default: total += 0;
                    break;
            }
            break;
    default:
        switch (y % 3)
        {
            case 0: total += 3;
                break;
            case 1: total += 2;
                break;
            case 2: total += 1;
                break;
            default: total += 0;
                break;
        }
        break;
    }

Sim, é um exemplo rudimentar, mas ilustra o ponto.

Portanto, uma conclusão pode ser usar switch / case para tipos simples que têm apenas um nível de profundidade, mas para comparações mais complexas e vários níveis aninhados, use as construções if / else clássicas?


fonte
-1: 1. O artigo ignorou completamente o Branch Prediction, 2. os algoritmos não são exatamente os mesmos (o único if-else no link já está codificado mais otimizado) e 3. as diferenças encontradas são tão pequenas que nada justifica o uso de código adequado e limpo (cerca de 4 ns em 10.000.000 chamadas entre switch e a mesma construção if-else)
Trojaner
Esse exemplo não será otimizado por causa de quão poucos casos o bloco de switch tem. Normalmente, após 5-6 elementos, ele gerará uma tabela de salto.
Antiduh
0

A única vantagem do caso if over é quando há um aumento perceptível da frequência de ocorrência do primeiro caso.

Não tenho certeza de onde está o limite, mas uso a sintaxe de caso, a menos que o primeiro "quase sempre" passe no primeiro teste.

Ralph
fonte