Site icon AranaCorp

Teste e otimização do código C/C++ com GNU

Otimização do código C para sistemas incorporados

A otimização do código C é necessária para os sistemas incorporados porque, para poupar nos custos de hardware, planeamos o mínimo necessário em termos de interface, memória e capacidade de computação. No entanto, para um computador fixo, são preferíveis tempos de execução baixos, para evitar sobrecarregar a CPU. A otimização terá dois objectivos concorrentes

Um programa pode ser tão rápido quanto possível ou tão pequeno quanto possível, mas não ambos. A otimização de um critério pode ter um impacto importante no outro.

Muitas vezes, o compilador encarrega-se de otimizar o código, mas é essencial optimizá-lo manualmente. Geralmente, concentramo-nos em otimizar as secções críticas do código e deixamos o compilador tratar do resto.

Embora existam boas práticas a ter em conta no desenvolvimento do código, este só deve ser optimizado quando estritamente necessário (limite de memória, execução lenta). Acima de tudo, o código deve ser legível, fácil de manter e funcional.

Melhorar a eficiência do código

Funções em linha

A palavra-chave inline é utilizada para dizer ao compilador para substituir uma chamada à função pelo código da função. Quando uma função está presente em poucas secções de código, mas é chamada um grande número de vezes, transformar a função numa função em linha pode melhorar o desempenho da execução do código.

Tabelas de pesquisa

As pesquisas podem ser utilizadas para substituir funções que requerem cálculos complicados por uma simples associação de variáveis. Por exemplo, pode substituir uma função sin() ou secções swtich por tabelas de escolha múltipla.

Código assembler manual

Um compilador transforma o código C em código de montagem optimizado. Quando a função é crítica, um programador experiente pode criar o seu próprio código de montagem.

Reduzir a dimensão do código (ROM)

Tamanho e tipo variáveis

Escolher a estrutura, o tipo e o tamanho corretos da variável necessária para armazenar dados melhora consideravelmente o desempenho do código.

Ir para a declaração

A função goto evita algoritmos de árvore complicados. Isto torna o código mais difícil de ler e pode ser a fonte de mais erros.

Evitar bibliotecas padrão

As bibliotecas padrão são geralmente muito grandes e computacionalmente intensivas porque tentam cobrir todos os casos. Desenvolver as suas próprias funções para satisfazer as suas necessidades específicas é uma boa forma de otimizar o seu código C#.

Reduzir a utilização da memória (RAM)

Palavras-chave Const e estáticas

Uma variável estática é uma variável que só pode ser acedida no contexto de uma função, mas cujo estado é mantido entre chamadas de função. Isto limita a utilização de variáveis globais.

Testar o desempenho do código

Medir o tempo de execução de uma secção de código utilizando a biblioteca time.h

#include <iostream>
#include <time.h>

clock_t start, end;
double cpu_time_used;
int main()
{
    std::cout << "Hello World" << std::endl;
    start = clock();
    for(;i<0xffffff;i++);
    end = clock();
    cpu_time_used = ((double) (end - start)) / CLOCKS_PER_SEC;
    printf("Execution time: %f seconds\n", cpu_time_used);
    return 0;
}

Utilizar o gprof

A ferramenta gprof fornecida com o compilador fornece tempos de execução para diferentes secções de código ou funções.

compilar com o sinalizador -pg para que o código gere um ficheiro gmon.out

g++ -g -Wall -no-pie -pg helloworld.cpp -o helloworld --include=include/utils.cpp  #compile for profiling
./helloworld  #execute and write gnom.out
gprof -b helloworld.exe gmon.out > perfo.txt  #translate gnom.out

Exemplo de saída do ficheiro perfo.txt: obtém-se uma tabela com o tempo de execução acumulado sobre o tempo total de execução, o número de chamadas e o tempo médio de execução.

Flat profile:

Each sample counts as 0.01 seconds.
  %   cumulative   self              self     total           
 time   seconds   seconds    calls   s/call   s/call  name    
 37.21      4.26     4.26        2     2.13     3.95  func1()
 31.79      7.90     3.64        1     3.64     3.64  new_func1()
 30.83     11.43     3.53                             func2()
  0.17     11.45     0.02                             main
  0.00     11.45     0.00        1     0.00     0.00  count_to(int, int)

Utilização da memória

A opção de compilação -stats fornece estatísticas gerais sobre o código

g++ -g -Wall -no-pie -pg helloworld.cpp -o helloworld --include=include/utils.cpp --stats

resultado

(No per-node statistics)
Type hash: size 32749, 23922 elements, 1.178881 collisions
DECL_DEBUG_EXPR  hash: size 1021, 0 elements, 0.000000 collisions
DECL_VALUE_EXPR  hash: size 1021, 18 elements, 0.031250 collisions
decl_specializations: size 8191, 6113 elements, 1.427515 collisions
type_specializations: size 8191, 3619 elements, 1.569889 collisions

******
time in header files (total): 0.862000 (38%)
time in main file (total): 1.416000 (62%)
ratio = 0.608757 : 1

******
time in ./include/utils.cpp: 0.002000 (0%)
time in <built-in>: 0.006000 (0%)
time in <command-line>: 0.000000 (0%)
time in <top level>: 0.009000 (0%)

As opções -fstack-usage e -Wstack-usage são usadas para verificar o uso da memória da pilha em tempo de compilação.

g++ -g -Wall -no-pie -pg helloworld.cpp -o helloworld --include=include/utils.cpp -fstack-usage
g++ -g -Wall -no-pie -pg helloworld.cpp -o helloworld --include=include/utils.cpp -Wstack-usage=32

O segundo verifica o tamanho da memória da pilha esperado para um determinado valor (32).

In file included from <command-line>:
./include/utils.cpp: In function 'void count_to(int, int)':
./include/utils.cpp:6:6: warning: stack usage is 64 bytes [-Wstack-usage=]
    6 | void count_to(int n, int delay)
      |      ^~~~~~~~
helloworld.cpp: In function 'void new_func1()':
helloworld.cpp:7:6: warning: stack usage is 64 bytes [-Wstack-usage=]
    7 | void new_func1(void)
      |      ^~~~~~~~~
helloworld.cpp: In function 'void func1()':
helloworld.cpp:17:6: warning: stack usage is 64 bytes [-Wstack-usage=]
   17 | void func1(void)
      |      ^~~~~
helloworld.cpp: In function 'void func2()':
helloworld.cpp:28:13: warning: stack usage is 64 bytes [-Wstack-usage=]
   28 | static void func2(void)
      |             ^~~~~
helloworld.cpp: In function 'int main()':
helloworld.cpp:40:5: warning: stack usage is 96 bytes [-Wstack-usage=]
   40 | int main(void)

Simulação da pegada de memória em hardware específico

É possível simular uma determinada peça de hardware com um script de ligação personalizado

myldscript.lds

MemoryStart AddressSize
Internal Flash0x00000000256 Kbytes
Internal SRAM0x2000000032 Kbytes
MEMORY
{
  rom      (rx)  : ORIGIN = 0x00000000, LENGTH = 0x00040000
  ram      (rwx) : ORIGIN = 0x20000000, LENGTH = 0x00008000
}

STACK_SIZE = 0x2000;

/* Section Definitions */
SECTIONS
{
    .text :
    {
        KEEP(*(.vectors .vectors.*))
        *(.text*)
        *(.rodata*)
    } > rom

    /* .bss section which is used for uninitialized data */
    .bss (NOLOAD) :
    {
        *(.bss*)
        *(COMMON)
    } > ram

    .data :
    {
        *(.data*);
    } > ram AT >rom

    /* stack section */
    .stack (NOLOAD):
    {
        . = ALIGN(8);
        . = . + STACK_SIZE;
        . = ALIGN(8);
    } > ram

    _end = . ;
}
gcc -g -c helloworld.cpp # compile object file
ld -o helloworld -T ldscript.lds helloworld.o --print-memory-usage #compile with specific link script
Memory region         Used Size  Region Size  %age Used
             rom:       12704 B       256 KB      4.85%
             ram:        4128 B        32 KB     12.60%

Fontes

Exit mobile version