Otimização de código em Linguagem C – Parte 2
- por Sergio Prado
Olá Pessoal!
No primeiro artigo, falei um pouco sobre como funciona o processo de otimização de código realizado pelo compilador, e dei algumas dicas sobre como otimizar a utilização de variáveis e funções. Para quem ainda não leu, a primeira parte do artigo pode ser acessada aqui.
Vamos começar este artigo falando das diretivas de otimização do compilador.
Otimização do compilador
O compilador pode ser instruído para compilar o programa com diferentes objetivos, normalmente priorizando tempo de processamento (speed optimization) ou tamanho de código (size optimization).
Como exemplo, vamos acompanhar a tabela abaixo, que lista informações sobre a compilação de dois programas distintos:
Os dois programas foram compilados com o mesmo compilador, utilizando os mesmos modelos de memória, mas com critérios de otimização diferentes. Perceba que o programa 1 ficou menor quando selecionada a otimização por tamanho de código, mas o programa 2 ficou menor quando selecionada a otimização por tempo de processamento! Isso significa que é sempre bom testar as duas opções no seu código e verificar os resultados, antes de definir a otimização ótima para seu projeto.
Dicas de programação
1. Use o tamanho certo para as variáveis do seu programa.
Basicamente, o compilador é preparado para compilar o código otimizado para a arquitetura-alvo do seu sistema. Isso significa que se você está trabalhando com um microprocessador de 8 bits, os cálculos terão maior performance se realizados com 8 bits de dados, em variáveis do tipo char. Da mesma forma, se você estiver trabalhando com processadores de 32 bits, os cálculos terão maior performance se realizados com variáveis do tipo int. Isso acontece porque o compilador realiza os cálculos com base no tamanho de seus registradores, que por sua vez são baseados na arquitetura da CPU. Se a sua CPU é de 32 bits, seus registradores serão de 32 bits, e se você está realizando cálculos com variáveis char, de 8 bits, nesta arquitetura, o compilador terá o esforço extra de converter o resultado final, de 32 bits, em uma variável de 8 bits.
2. Incremento de variáveis
É muito mais eficiente incrementar variáveis com o uso de operadores de incremento (ex: ind++) do que o comando de atribuição (ex: ind = ind + 1). Isso porque a maioria dos compiladores usam , se disponível, a instrução de incremento do processador, o que torna o código-objeto gerado muito mais otimizado, conforme exemplo abaixo:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/* incremento com atribuicao */ x = x + 1; /* codigo gerado em assembly * mov A, x ; carrega o valor de x no registrador A * add A, 1 ; soma 1 ao registrador A * mov x, A ; carrega de volta o valor do registrador A em x */ /* operador de incremento */ x++; /* codigo gerado em assembly * inc x; incrementa x em 1 */ |
3. Use protótipos para funções
Protótipo de função foi introduzido na definição do padrão ANSI para linguagem C como uma forma de melhorar a verificação de tipos. Se o protótipo da função não é devidamente definido, o compilador precisará “adivinhar” quais os tipos de dados com que sua função trabalha, o que diminui bastante a performance do sistema.
4. Não escreva códigos muito “inteligentes”!
Alguns programadores acreditam que, ao escrever em poucas linhas de código fazendo uso “inteligente” de construções complexas em linguagem C, deixarão o código menor e mais rápido. Compare a funcao1() com a funcao2() abaixo. O objetivo das duas funções é setar o bit menos significativo de uma variável se os 21 bits menos significativos de outra variável forem diferentes de zero. Apesar de ambos estarem realizando corretamente a mesma tarefa, a funcao1() irá gerar um código maior, porque o compilador irá montar duas estrutura condicionais por causa dos dois operadores “!!”, além de deixar o código quase indecifrável! A funcao2() é mais clara, fácil de dar manutenção, e possui melhor performance.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
void funcao1() { unsigned long int a; unsigned char b; /* se bits 0..20 de a != 0, seta bit 1 de b */ b |= !!(a << 11); ... } void funcao2() { unsigned long int a; unsigned char b; /* se bits 0..20 de a != 0, seta bit 1 de b */ if ( (a & 0x1FFFFF) != 0) b |= 0x01; ... } |
5. Cuidado com o uso de bibliotecas
Algumas vezes, o uso de apenas uma função de uma biblioteca, como printf() pode trazer junto com ela, implicitamente, outras funções necessárias para sua utilização, e isso pode aumentar bastante o tamanho do seu código. Quando tratamos de sistemas embarcados com poucos recursos disponíveis, às vezes é melhor implementar uma função simples do tipo strcat(), do que utilizar a de uma biblioteca que pode fazer o tamanho do código crescer.
Estes dois artigos procuraram trazer os conceitos básicos de um compilador C para que possamos entender sua estrutura interna e utilizá-lo da melhor forma possível de modo a gerar um código mais otimizado e eficiente. Existem muitas técnicas de otimização de software, a maioria delas aplicáveis no desenvolvimento de sistemas embarcados. Exemplifiquei apenas algumas destas técnicas de programação, que podem ser aplicadas no nosso dia-a-dia, para melhorar a performance e a eficiência de nossos projetos de software.
E você, e que costuma fazer para otimizar seu código em linguagem C?
Um abraço e até a próxima!
Sergio Prado