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
- réduire la quantité de mémoire utilisée par un programme
- augmenter la vitesse d’exécution des fonctions du programme
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
Memory | Start Address | Size |
Internal Flash | 0x00000000 | 256 Kbytes |
Internal SRAM | 0x20000000 | 32 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%