No nível mais baixo (no hardware), sim, se são caros. Para entender o porquê, você precisa entender como funcionam os pipelines .
A instrução atual a ser executada é armazenada em algo tipicamente chamado de ponteiro de instrução (IP) ou contador de programa (PC); esses termos são sinônimos, mas termos diferentes são usados com arquiteturas diferentes. Para a maioria das instruções, o PC da próxima instrução é apenas o PC atual mais o comprimento da instrução atual. Para a maioria das arquiteturas RISC, as instruções têm comprimento constante, de modo que o PC pode ser incrementado em um valor constante. Para arquiteturas CISC, como x86, as instruções podem ter comprimento variável, de modo que a lógica que decodifica a instrução precisa descobrir quanto tempo a instrução atual dura para encontrar a localização da próxima instrução.
Para instruções de desvio , entretanto, a próxima instrução a ser executada não é o próximo local após a instrução atual. Ramificações são gotos - elas dizem ao processador onde está a próxima instrução. Ramificações podem ser condicionais ou incondicionais e o local de destino pode ser fixo ou calculado.
Condicional versus incondicional é fácil de entender - um desvio condicional só é obtido se uma certa condição for mantida (como se um número é igual a outro); se o desvio não for obtido, o controle prossegue para a próxima instrução após o desvio normalmente. Para ramificações incondicionais, a ramificação é sempre tomada. Ramificações condicionais aparecem em if
instruções e nos testes de controle for
e while
loops. Ramificações incondicionais aparecem em loops infinitos, chamadas de função, retornos de função break
e continue
instruções, a infame goto
instrução e muito mais (essas listas estão longe de ser exaustivas).
O alvo da filial é outra questão importante. A maioria das ramificações tem um destino de ramificação fixo - elas vão para um local específico no código que é fixado em tempo de compilação. Isso inclui if
instruções, loops de todos os tipos, chamadas de função regulares e muito mais. Os ramos calculados calculam o destino do ramo em tempo de execução. Isso inclui switch
instruções (às vezes), retorno de uma função, chamadas de função virtual e chamadas de ponteiro de função.
Então, o que tudo isso significa para o desempenho? Quando o processador vê uma instrução de ramificação aparecer em seu pipeline, ele precisa descobrir como continuar a preencher seu pipeline. Para descobrir quais instruções vêm após a ramificação no fluxo do programa, ele precisa saber duas coisas: (1) se a ramificação será tomada e (2) o destino da ramificação. Descobrir isso é chamado de previsão de ramificação e é um problema desafiador. Se o processador adivinhar corretamente, o programa continua em velocidade total. Se, em vez disso, o processador adivinhar incorretamente , ele apenas gastou algum tempo computando a coisa errada. Agora, ele precisa liberar seu pipeline e recarregá-lo com instruções do caminho de execução correto. Resumindo: um grande sucesso de desempenho.
Portanto, o motivo pelo qual se as declarações são caras é devido a erros de previsão do ramo . Isso está apenas no nível mais baixo. Se você está escrevendo um código de alto nível, não precisa se preocupar com esses detalhes. Você só deve se preocupar com isso se estiver escrevendo um código extremamente crítico para o desempenho em C ou assembly. Se for esse o caso, escrever código sem ramificação pode frequentemente ser superior ao código que ramifica, mesmo se várias instruções adicionais forem necessárias. Existem alguns truques-girando bit que você pode fazer para calcular coisas como abs()
, min()
e max()
sem ramificação.