Site icon AranaCorp

Test et optimisation d’un code C/C++ avec GNU

Dans le cas de développement de code C/C++ sur systèmes embarqués, notamment, l’optimisation et le test du code sont des points essentiels afin de limiter l’empreinte mémoire et le temps d’exécution.

Optimisation du code C pour les systèmes embarquées

L’optimisation du code C est nécessaire pour les systèmes embarqués car, afin de gagner sur le coût du hardware, on prévoit le strict nécessaire en terme d’interface, de mémoire et de capacité de calcul. Pour un ordinateur fixe, on va tout de même préférer des temps d’exécution faibles afin de ne pas encombrer la charge CPU. L’optimisation va avoir deux objectifs concurrentiels

Une programme peut être le plus rapide possible ou le plus petit possible mais pas les deux. L’optimisation d’un des critères peut fortement impacter l’autre.

Le compilateur se charge souvent d’optimiser le code mais il est primordiale de l’optimiser manuellement. On va généralement se concentrer sur l’optimisation des sections critiques du code et laisser le compilateur se charger du reste.

Même s’il y a de bonnes pratiques à garder à l’esprit lorsqu’on développe un code, un code ne devrait être otpimisé que lorque c’est strictement nécessaire (limite mémoire, lenteur d’exécution). Un code doit avant tout être lisible, maintenable et fonctionnel.

Améliorer l’efficacité du code

Les fonctions inline

Le mot clé inline permet de spécifier au compilateur de remplacer un appel à la fonction par le code de la fonction. Lorsqu’une fonction est présente dans quelques sections de code mais appelée un grand nombre de fois, transformer la fonction en fonction inline peut améliore les performances d’exécution du code

Les tables lookup

Les lookup permettent de remplacer des fonctions demandant un calcul compliqué par une simple association de variables. On peut ainsi remplacer une fonction sin() ou des sections swtich par des tables à choix multiples.

Code assembleur manuel

Un compilateur transforme le code C en code assembleur optimisé. Un développeur confirmé peut, lorsque la fonction est critique, créer son propre code assembleur.

Réduire la taille du code (ROM)

Taille et type de variable

Choisir correctement la structure, le type et la taille de variable nécessaire pour stocker les données améliore considérablement les performances du code.

Goto statement

La fonction goto permet d’éviter des algorithmes d’arborescences compliqués. Cela rend le code plus difficile à lire et peut être la source de plus d’erreur.

Éviter les librairies standards

Les librairies standards sont généralement lourdes en taille et gourmandes en calculs car elles tentent de couvrir tous les cas. Développer ses propres fonctions correspondant à son strict besoin est un bon moyen d’optimiser son code C/C++

Réduire l’usage de la mémoire (RAM)

Mots-clés const et static

Une variables static correspond à une variable uniquement accessible dans le contexte d’une fonction mais dont l’état est maintenu entre les appels de la fonction. Cela permet de limiter l’utlisation de variable globale

Tester les performances du code C/C++

Mesurer le temps d’exécution d’une section de code à l’aide de la librairie 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;
}

Utilisation de gprof

L’outil gprof fournit avec le compilateur permet d’avoir les temps d’exécution de différentes sections de code ou fonctions.

compiler avec le flag -pg pour que le code génère un fichier 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

Exemple de résultat de fichier perfo.txt: vous obtenez un tableau contenant le temps d’exécution cumulé sur la durée total d’exécution, le nombre d’appels et le temps d’exécution moyen.

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)

Utilisation de la mémoire

L »option de compilation –stats donne des statistiques générales sur le code

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

resultat

(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%)

Les options -fstack-usage et -Wstack-usage permet de vérifier l’usage de la mémoire stack à la compilation

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

La deuxième vérifie la taille de la mémoire stack attendu pour une valeur données (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)

Simulation de l’empreinte mémoire sur un hardware spécifique

Il est possible de simuler un hardware particulier avec un link script personnalisé

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%

Sources

Quitter la version mobile