Performance no chaveamento de enumerações (LeonardoStabile)
De ccppbrasil.org
Ou ainda: qual é mais rápido no chaveamento de enumerações: if/else ou switch?
Tabela de conteúdo |
Introdução
No desenvolvimento de sistemas embutidos, sempre lido com distribuição de mensagens e tratamento de eventos entre componentes do sistema. Além disso, sempre codifico chaveamento de enumerações, que podem variar entre identificadores de dispositivos, canais de comunicação, portas, timers, entre outros. Como nessa área a performance do código conta muito, a verificação de assembly sempre é bem vinda! Recentemente, analisando código legado do sistema, notei que o tratamento de enumerações é quase sempre feito por switch, mas algumas vezes o cascateamento de if/else é usado. Resolvi então verificar qual dos dois é melhor na prática, tendo em vista performance para velocidade.
Cenário
Na prática eu não utilizo o Visual Studio, o compilador e a IDE são outros, mas para efeito de comparação já é um começo. Foi utilizado o Visual C++ 2005, com duas variações de compilação:
- sem otimização
- /Ot (favorecer velocidade) e /GL (otimização geral do sistema)
Ambas variações geraram assembly idêntico.
Código fonte
#include <iostream>
using namespace std;
void my_if( int id )
{
if( id == 76 ||
id == 32 ||
id == 43 )
{
cout << "opcao 1";
}
else if( id == 56 ||
id == 23 ||
id == 89 )
{
cout << "opcao 2";
}
else if( id == 12 ||
id == 34 ||
id == 98 ||
id == 21 )
{
cout << "opcao 3";
}
else
{
cout << "opcao 4";
}
}
void my_switch( int id )
{
switch( id )
{
case 76:
case 32:
case 43:
cout << "opcao 1";
break;
case 56:
case 23:
case 89:
cout << "opcao 2";
break;
case 12:
case 34:
case 98:
case 21:
cout << "opcao 3";
break;
default:
cout << "opcao 4";
break;
}
}
int main()
{
int id = rand() % 100;
my_if( id );
my_switch( id );
return 0;
}
Análise dos Resultados e Conclusão
O assembly gerado pelo cascateamento de if/else foi bastante óbvio e didático:
. . . ; conjunto 1 de condições 0044F853 cmp dword ptr [id],4Ch 0044F857 je my_if+15h (44F865h) 0044F859 cmp dword ptr [id],20h 0044F85D je my_if+15h (44F865h) 0044F85F cmp dword ptr [id],2Bh 0044F863 jne my_if+29h (44F879h) ; bloco do conjunto 1 de condições 0044F865 - 0044F877 ; conjunto 2 de condições 0044F879 cmp dword ptr [id],38h 0044F87D je my_if+3Bh (44F88Bh) 0044F87F cmp dword ptr [id],17h 0044F883 je my_if+3Bh (44F88Bh) 0044F885 cmp dword ptr [id],59h 0044F889 jne my_if+4Fh (44F89Fh) ; bloco do conjunto 2 de condições 0044F88B - 0044F89D ; conjunto 3 de condições 0044F89F cmp dword ptr [id],0Ch 0044F8A3 je my_if+67h (44F8B7h) 0044F8A5 cmp dword ptr [id],22h 0044F8A9 je my_if+67h (44F8B7h) 0044F8AB cmp dword ptr [id],62h 0044F8AF je my_if+67h (44F8B7h) 0044F8B1 cmp dword ptr [id],15h 0044F8B5 jne my_if+7Bh (44F8CBh) ; bloco do conjunto 3 de condições 0044F8B7 - 0044F8C9 ; bloco padrão (caso 'default') 0044F8CB - 0044F8DA . . .
O valor é comparado com o primeiro literal do primeiro grupo de condições, se for igual o bloco é executado senão a comparação do próximo é feita. Se o último literal do grupo for testado e não for igual ao valor, o próximo grupo de condições é comparado. Isso era o esperado, dada a robustez do if. A princípio eu esperava o mesmo do switch, pelo menos sem otimização alguma de código, mas não foi o que aconteceu:
. . . 0044F75B mov eax,dword ptr [id] 0044F75E mov dword ptr [ebp-4],eax 0044F761 mov ecx,dword ptr [ebp-4] 0044F764 sub ecx,0Ch 0044F767 mov dword ptr [ebp-4],ecx 0044F76A cmp dword ptr [ebp-4],56h 0044F76E ja $LN2+14h (44F7BDh) 0044F770 mov edx,dword ptr [ebp-4] 0044F773 movzx eax,byte ptr (44F7F0h)[edx] 0044F77A jmp dword ptr (44F7E0h)[eax*4] . . .
Nota-se que foi gerado um código nada didático, pelo menos à primeira vista. Pesquisando um pouco e logo encontrei: como o switch lida somente com dados integrais nas condições, a melhor otimização é feita, através da criação de uma tabela hash de jumps para os casos tratados, cada grupo de condição pulando para uma parte do código específica. Melhor impossível, até porque o código independe da quantidade de enumerações tratadas (o tamanho é sempre o mesmo), diferente do if/else, que percorre linearmente para comparar. Interessante notar que a tabela de jumps só é feita quando o compilador acha adequado, senão o mesmo algoritmo linear é usado para switchs. Isso acontece quando a quantidade de condições é muito pequena (1, 2 ou 3).
Com isso só posso concluir que para um compilador robusto e esperto como o do Visual C++, usar switch sempre é a melhor escolha, pois ele saberá escolher a melhor abordagem para o código. No meu caso, o compilador usado para produção do sistema é bem menos robusto, mas a mesma regra se aplica. Portanto, para chaveamentos, prefira sempre switch. Do ponto de vista semântico também é muito melhor, pois é muito mais identificável no código.
Os ganhos reais podem parecer pequenos, para a maioria das aplicações eles realmente são, mas fazem a diferença em alguns pontos críticos de sistemas de comunicação, que devem manter baixa latência, ou em sistemas de tempo real.
