A implementação a seguir do square produz uma série de instruções cmp / je, como eu esperaria de uma instrução if encadeada:
int square(int num) {
if (num == 0){
return 0;
} else if (num == 1){
return 1;
} else if (num == 2){
return 4;
} else if (num == 3){
return 9;
} else if (num == 4){
return 16;
} else if (num == 5){
return 25;
} else if (num == 6){
return 36;
} else if (num == 7){
return 49;
} else {
return num * num;
}
}
E o seguinte produz uma tabela de dados para retorno:
int square_2(int num) {
switch (num){
case 0: return 0;
case 1: return 1;
case 2: return 4;
case 3: return 9;
case 4: return 16;
case 5: return 25;
case 6: return 36;
case 7: return 49;
default: return num * num;
}
}
Por que o gcc não consegue otimizar o de cima para o de baixo?
Desmontagem para referência: https://godbolt.org/z/UP_igi
EDIT: curiosamente, o MSVC gera uma tabela de salto em vez de uma tabela de dados para o caso do comutador. E, surpreendentemente, o clang os otimiza para o mesmo resultado.
c++
c
gcc
optimization
compiler-optimization
chacham15
fonte
fonte
return
; os casos não têmbreaks
, portanto, o switch também possui uma ordem específica de execução. A cadeia if / else tem retornos em todos os ramos, a semântica neste caso é equivalente. A otimização não é impossível . Como um contra-exemplo, o icc não otimiza nenhuma das funções.Respostas:
O código gerado para
switch-case
convencionalmente usa uma tabela de salto. Nesse caso, o retorno direto através de uma tabela de consulta parece ser uma otimização, aproveitando o fato de que todos os casos aqui envolvem um retorno. Embora o padrão não garanta esse efeito, eu ficaria surpreso se um compilador gerasse uma série de comparações em vez de uma tabela de salto para um caso de comutação convencional.Agora, chegando
if-else
, é exatamente o oposto. Embora sejaswitch-case
executado em tempo constante, independentemente do número de ramificações,if-else
é otimizado para um número menor de ramificações. Aqui, você esperaria que o compilador gere basicamente uma série de comparações na ordem em que você as escreveu.Então, se eu tivesse usado
if-else
porque espero maioria das chamadas parasquare()
ser para0
ou1
e raramente por outros valores, em seguida, 'otimizar' a um table-lookup poderia realmente fazer com o meu código para executar mais lento do que eu esperava, derrotando o meu propósito para o uso de umaif
vez de umswitch
. Portanto, embora seja discutível, acho que o GCC está fazendo a coisa certa e o clang está sendo excessivamente agressivo em sua otimização.Alguém, nos comentários, compartilhou um link em que o clang faz essa otimização e também gera código baseado em tabela de pesquisa
if-else
. Algo notável acontece quando reduzimos o número de casos para apenas dois (e o padrão) com o clang. Mais uma vez, gera código idêntico para if e switch, mas desta vez passa para compara e move, em vez da abordagem da tabela de pesquisa, para ambos. Isso significa que mesmo o clang que favorece a troca sabe que o padrão 'if' é mais ideal quando o número de casos é pequeno!Em resumo, uma sequência de comparações para
if-else
e uma tabela de salto paraswitch-case
é o padrão padrão que os compiladores tendem a seguir e os desenvolvedores tendem a esperar quando escrevem código. No entanto, em certos casos especiais, alguns compiladores podem optar por quebrar esse padrão quando acharem que ele fornece uma melhor otimização. Outros compiladores podem simplesmente optar por seguir o padrão de qualquer maneira, mesmo que aparentemente sejam abaixo do ideal, confiando no desenvolvedor para saber o que ele deseja. Ambas são abordagens válidas com suas próprias vantagens e desvantagens.fonte
0
e1
)?if
é obviamente mais rápido? Agora, aqui está um exemplo de plataforma em que 0 e 1 seriam mais rápidos ao usar doif
que ao usar o switch: godbolt.org/z/wcJhvS (Observe que também há várias outras otimizações em jogo aqui)if
instruções explícitas ou automaticamente pelo compilador. Como não sou especialista em ARM, não tenho muita certeza se a reivindicação que você está fazendoswitch
é mais rápida do queif
é verdade. Isso dependeria da penalidade para ramos imprevisíveis, e isso realmente dependeria de qual ARM.Uma lógica possível é que, se os valores baixos de
num
são mais prováveis, por exemplo sempre 0, o código gerado para o primeiro pode ser mais rápido. O código gerado para o switch leva tempo igual para todos os valores.Comparando os melhores casos, de acordo com esta tabela . Veja esta resposta para a explicação da tabela.
Se
num == 0
, para "se" você tiver xor, teste, je (com salto), ret. Latência: 1 + 1 + salto. No entanto, xor e test são independentes, portanto a velocidade de execução real seria mais rápida que 1 + 1 ciclos.Se
num < 7
, para "switch" você tiver mov, cmp, ja (sem salto), mov, ret. Latência: 2 + 1 + sem salto + 2.Uma instrução de salto que não resulta em salto é mais rápida do que uma instrução que resulta em salto. No entanto, a tabela não define a latência para um salto, portanto, não está claro para mim qual é o melhor. É possível que o último seja sempre melhor e o GCC simplesmente não consiga otimizá-lo.
fonte