Olá Pes­soal!

Sis­temas embar­ca­dos têm como uma de suas prin­ci­pais car­ac­terís­ti­cas a lim­i­tação de recur­sos com­puta­cionais. Como estes dis­pos­i­tivos pos­suem apli­cações bem especí­fi­cas, são pro­je­ta­dos na medida para cumprir a função para o qual foram designados.

Seja para diminuir cus­tos do pro­jeto, tempo de desen­volvi­mento, tamanho do dis­pos­i­tivo ou con­sumo elétrico, nor­mal­mente são lim­i­ta­dos pela quan­ti­dade de memória, capaci­dade de proces­sa­mento e dis­pos­i­tivos de I/O. Por­tanto, escr­ever código para estes dis­pos­i­tivos requer atenção espe­cial quanto a uti­liza­ção e geren­ci­a­mento efi­ciente dos recur­sos disponíveis.

Nosso foco aqui é soft­ware  desen­volvido em lin­guagem C, uma vez que grande parte do soft­ware desen­volvido para dis­pos­i­tivos embar­ca­dos ainda é feita nesta lin­guagem. Entre­tanto, grande parte dos con­ceitos podem ser apli­ca­dos igual­mente em qual­quer outra lin­guagem estruturada.

A otimiza­ção do código está inti­ma­mente rela­cionada à maneira como é real­izada a “trans­for­mação”, ou com­pi­lação, do código-fonte no arquivo binário que será car­regado para a memória do dis­pos­i­tivo. O com­pi­lador é o soft­ware respon­sável por esta trans­for­mação, e é por ele que ini­cia­re­mos nosso trabalho.

O com­pi­lador C

O com­pi­lador C é a fer­ra­menta que irá trans­for­mar suas idéias e algo­rit­mos (código-fonte C), na sua apli­cação. O com­pi­lador real­iza uma série de trans­for­mações no código-fonte para gerar o mel­hor código pos­sível, como por exem­plo sal­var var­iáveis em reg­istradores ao invés de usar a memória para mel­ho­rar o tempo de acesso (otimiza­ção de proces­sa­mento) ou remover código inútil do pro­grama (otimiza­ção de espaço).

É necessário con­hecer o fun­ciona­mento interno do com­pi­lador para que o código possa ser com­pi­lado da forma mais otimizada pos­sível. Em um com­pi­lador C mod­erno, o processo de com­pi­lação envolve basi­ca­mente os seis pas­sos abaixo, na ordem apresentada:

1. Pré-processamento: Con­ver­são do código-fonte em uma lin­guagem inter­mediária. É aqui que as dire­ti­vas de com­pi­lação são proces­sadas e a sin­taxe do código é verificada.

2. Otimiza­ção de alto nível: É nesta etapa que o com­pi­lador real­iza a primeira otimiza­ção do código, em cima do código-fonte da apli­cação. Ver­e­mos mais adi­ante algu­mas das téc­ni­cas para aux­il­iar o com­pi­lador neste tra­balho de otimização.

3. Ger­ação do código: Ger­ação do código de máquina para a arquitetura-alvo. Nesta fase o com­pi­lador faz todas as con­ver­sões necessárias para a arquite­tura em questão. Expressões arit­méti­cas de 32 bits em proces­sadores de 8 bits são con­ver­tidas em expressões arit­méti­cas de 8 bits, por exemplo.

4. Otimiza­ção de baixo nível: Nesta fase a otimiza­ção é real­izada em cima do código-objeto ger­ado na fase ante­rior, como por exem­plo removendo instruções não usadas ou redundantes.

5. Assem­bler: Nesta etapa o com­pi­lador gera o arquivo-objeto cor­re­spon­dente ao arquivo-fonte C compilado.

6. Link­agem: Por último, o com­pi­lador faz a link­agem de todos os arquivos-objeto em um único arquivo binário para ser car­regado na memória do dispositivo.

Podemos perce­ber que, den­tre as seis eta­pas no processo de com­pi­lação descritas, quando pen­samos em otimiza­ção de código, deve­mos focar nas eta­pas 2 e 4. Ver­e­mos a seguir algu­mas téc­ni­cas que podem aux­il­iar e instruir o com­pi­lador na tarefa de otimizar o código gerado.

Var­iáveis

O tempo de acesso aos reg­istradores da CPU é nor­mal­mente muito mais rápido do que o tempo de acesso à memória  de dados, e o proces­sador pos­sui mel­hor per­for­mance quando real­iza cál­cu­los uti­lizando reg­istradores ao invés de memória. Por este motivo, o com­pi­lador procura alo­car var­iáveis locais e parâmet­ros para funções em reg­istradores para mel­ho­rar a per­for­mance do sis­tema. Mas como a quan­ti­dade de reg­istradores é lim­i­tada, nem sem­pre é pos­sível alo­car todas as var­iáveis em registradores.

O com­pi­lador pre­cisa decidir quais var­iáveis irão ser aces­sadas através de reg­istradores e quais dev­erão ser man­ti­das em memória. Podemos instruir o com­pi­lador através da palavra-chave reg­is­ter, con­forme mostra a listagem abaixo:

1
2
3
4
5
6
7
8
9
int exp(register int base, register int expoente)
{
    register int resultado = 1;
 
    for (;  expoente;  expoente--)
        resultado *= base;
 
    return(resultado);
}


Tanto os parâmetros da função (base, expoente) quanto a variável local (resultado) foram definidas como register , instruindo o compilador para alocá-las em registradores. Devemos ressaltar que nem sempre será possível alocar todas as variáveis em registradores. A palavra-chave register é apenas uma orientação para auxiliar o compilador a decidir quais variáveis são mais prioritárias para a alocação em registradores. Por este motivo é importante ter um código modular, com funções pequenas e específicas, para que a quantidade de variáveis locais também seja pequena, aumentando as chances de que estas sejam otimizadas através do armazenamento em registradores.

Funções

Uma chamada de função é um processo com­plexo e deve ser lev­ado em con­sid­er­ação quando pen­samos em per­for­mance. Basi­ca­mente, antes de uma chamada à função o com­pi­lador salva em memória (ou reg­istradores) os argu­men­tos da função e o endereço de retorno. Essa região de memória é chamada de stack. Den­tro da função chamada, os reg­istradores são salvos, parâmet­ros são lidos do stack ou de reg­istradores, e var­iáveis locais são alo­cadas em memória ou em reg­istradores (se disponíveis). Podemos perce­ber que, para funções com uma quan­ti­dade grande de parâmet­ros, o custo de uma chamada pode ser enorme para a per­for­mance do sis­tema. Uma das téc­ni­cas para mel­ho­rar a per­for­mance é diminuir a quan­ti­dade de parâmet­ros pas­sa­dos para funções. Podemos perce­ber com­para­ndo as duas funções abaixo, que o custo de chamada da função atualiza_dados(), com 5 parâmet­ros, é muito maior que o custo de chamada da função atualiza_dados_otimizada(), que recebe ape­nas um pon­teiro para uma estru­tura de dados como parâmetro.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void atualiza_dados(char *nome, char *endereco, char *cidade,
                    char *telefone, double salario)
{
    ...
}
 
struct Dados {
    char *nome;
    char *endereco;
    char *cidade;
    char *telefone;
    double salario;
};
 
void atualiza_dados_otimizada(struct *Dados)
{
    ...
}

Uma outra técnica de otimização é o uso do recurso de inlining de funções. É uma boa prática de programação quebrar o código em pequenas funções, porém o custo de chamada das funções pode diminuir a performance do sistema. Para minimizar este problema de performance, podemos utilizar o recurso de inlining de função, onde a chamada à função é substituída por uma cópia do seu código. Em código C, podemos empregar este recurso através da definição de macros, conforme ilustra o código abaixo. No primeiro caso, a função soma() será chamada 100 vezes, diminuindo a performance do sistema. No segundo caso, usamos a macro SOMA(), eliminando a chamada à função e conseqüentemente aumentando a performance do sistema.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#define SOMA(a, b) (a + b)
 
int soma(int a, int b)
{
    return(a + b);
}
 
int main()
{
    int i, res, a = 1;
 
    /* primeiro caso */
    for (i = 0; i < 100; i++)
        res = soma(a, res);
 
    /* segundo caso */
    for (res = 0, i = 0; i < 100; i++)
        res = SOMA(a, res);
 
    ...
}

Na parte 2 deste artigo, vamos falar sobre diretivas de otimização do compilador e algumas técnicas de desenvolvimento visando a otimização de tamanho e performance do código gerado.  Até lá!

Um abraço,

Ser­gio Prado

VN:F [1.9.17_1161]
Rat­ing: 0.0/10 (0 votes cast)

Sem posts relacionados.

  • http://eletrica.wikidot.com/ Sujdik

    Gostei do seu artigo.

    (vi o link no grupo de e-mails sis_embarcados)

    Estou desen­vol­vendo meu tra­balho de con­clusão de curso na fac­ul­dade, e sem­pre procuro otimizar meus códi­gos.
    Alguns cole­gas que me procu­ram para ori­en­tar na cod­i­fi­cação, apre­sen­tam códi­gos para micro­con­tro­ladores e out­ros dis­pos­i­tivos embar­ca­dos sem qual­quer tipo de otimiza­ção. O resul­tado disso foi, no meu próprio grupo, um código não rodar dev­ido a falta de memória.
    Após aplicar proces­sos de otimiza­ção, con­segui lib­erar 40% da ram con­sum­ida pelo código de meu colega e então ter nossa apli­cação rodando

    Recomendei a leitura de seu artigo para todos os cole­gas de curso que estão no mesmo semestre.

    Abraço, con­tinue com o bom trabalho.

    VA:F [1.9.17_1161]
    Rating: 0.0/5 (0 votes cast)
  • http://www.embarcados.com.br ser­gio­prado

    É ver­dade Sujdik, já cheguei a tra­bal­har em pro­je­tos com ape­nas 2K de RAM, e chegava a ter disponivel ape­nas 40 bytes de stack. Cada chamada de função era um sufoco para evi­tar stack overflow…:)

    Bons estu­dos e se pre­cisar de ajuda sobre o assunto é só falar.

    Um abraço!

    VA:F [1.9.17_1161]
    Rating: 0.0/5 (0 votes cast)
  • Alexan­dre

    Grande Ser­gio.
    Ótimo artigo.
    Bom para os ini­ciantes enten­derem todo processo e para os mais expe­ri­entes lem­brarem de dicas preciosas.

    Abraço.

    VA:F [1.9.17_1161]
    Rating: 0.0/5 (0 votes cast)
  • Pingback: Otimização de código em Linguagem C - Parte 2

  • Jose­cley

    Boa Tarde Ser­gio Prado.
    Eu estou desen­vol­vendo um pro­jeto que tem varias funções .c e com­para­ndo o mesmo codigo com um em for­tran percebi que o codigo em C está muito lento. Eu uso somas em vetores 1D E 2D.
    Vc pode­ria da-me uma dica de como otimizar o meu codigo?
    abraços.

    VA:F [1.9.17_1161]
    Rating: 0.0/5 (0 votes cast)
    • http://www.sergioprado.org ser­gio­prado

      Olá Jose­cley,

      Me envie um email com seu código para que eu possa dar uma olhada.

      Um abraço!

      VA:F [1.9.17_1161]
      Rating: 0.0/5 (0 votes cast)
  • Leonardo Faria Lessa

     BOa Tarde, estou fazendo um tra­balho na fac­ul­dade e estou com difi­cul­dades de falar sobre os 3 tipos de otimiza­ção em c++, pode­ria me aju­dar sobre esse assunto, ficaria muito grato!

    VA:F [1.9.17_1161]
    Rating: 0.0/5 (0 votes cast)
    • http://www.sergioprado.org Ser­gio Prado

      Olá Leonardo,

      Tenho certeza que você encon­trará bas­tante infor­mação sobre este tema na inter­net, como no link abaixo:

      http://www.tantalon.com/pete/cppopt/main.htm

      Um abraço.

      VA:F [1.9.17_1161]
      Rating: 0.0/5 (0 votes cast)