LOCODUINO

Bibliothèque MemoryUsage

Ou comment gérer les fuites.

.
Par : Thierry

DIFFICULTÉ :

La mémoire reste une denrée très rare sur les petits Arduino. Elle l’est moins sur les modèles les plus évolués comme le Due ou les Teensy. Pourtant, quelle que soit la plateforme, la gestion de la mémoire doit être prise au sérieux...

Lorsque vous compilez un croquis, l’IDE Arduino, dans sa grande bonté, vous donne quelques chiffres destinés à vous aider à comprendre le résultat de la compilation...

PNG - 4.1 kio

On voit ici que 1884 octets de mémoire programme (dite aussi mémoire flash) ont été utilisés sur les 32256 disponibles. Cette partie de la mémoire n’est jamais modifiée par le programme lui même. Ça veut dire que vous pouvez la remplir avec du code exécutable jusqu’au dernier octet !
La seconde phrase est plus inquiétante : sur les 2048 octets de mémoire dynamique (aussi appelée SRAM) qui contient les données volatiles pendant l’exécution du programme, il n’en reste que 1866. Les 182 octets sont vraisemblablement utilisés par des variables globales du code compilé, comme des liste d’entiers ou des chaînes de caractères... C’est la partie immuable de la SRAM, remplie d’office par le programme. Ces données globales ne sont pas forcément les vôtres, les bibliothèques en utilisent souvent pour pouvoir fonctionner.

Il reste plein de place alors ?

Si c’était si simple, mais non, cela ne représente que l’un des usages de la mémoire vive. Il y a en réalité trois zones dans cette mémoire :

PNG - 8 kio

Comme on peut le deviner, la somme de ces trois zones ne doit pas dépasser le total de mémoire SRAM disponible sur le micro-contrôleur. Le problème vient des zones de taille variable que sont les allocations et la pile d’exécution.

Bombe à fragmentation !

Le mémoire SRAM est un espace linéaire qui se remplit par défaut depuis le début de la zone (adresse 0) tant qu’il y a de la place... Les allocations faites avec des ’new’ ou des appels aux fonctions ’alloc’ du C standard réservent des adresses pour vous. La valeur de retour de ces fonctions est l’adresse du début de zone. Prenons une photo de l’espace mémoire à un moment précis de l’exécution dans une configuration simple (qui a dit simpliste ?) :

PNG - 10.9 kio

La zone des variables globales fait 182 octets de long. Certaines allocations ont déjà été faites par le setup() de votre fichier .ino et ont utilisé 318 octets (tiens, ça fait 500 au total, curieux hasard...). A l’autre bout, la pile (nous verrons plus loin son rôle) qui part toujours de la fin de la mémoire et occupe 48 octets, ce qui laisse 1500 octets disponibles pour continuer l’exécution. Allouons 800 octets :

char *text = new char[800];

PNG - 14.4 kio

Le nouvel espace réservé se place à la suite de ce qui a déjà été alloué.... Si je libère cette mémoire,

delete[] text;

je reviens à l’état précédent, la mémoire disponible retombe à 1500 octets...

PNG - 10.9 kio

Notez qu’un new[] (avec les crochets !) doit être détruit par un delete[] ! Allouons maintenant 500 octets, puis encore 500 octets :

char *text1 = new char[500];
char *text2 = new char[500];
PNG - 16.6 kio

La mémoire disponible est bien descendue à 300. Libérons par un ’delete[]’ la première allocation :

PNG - 13.1 kio

On peut voir qu’un trou s’est formé. Une allocation ne peut jamais être déplacée ! La mémoire disponible est bien de 1000 octets, mais en deux parties de 500 octets chacune. Si je dois allouer à nouveau mes 800 octets, l’allocation échouera. Une zone allouée est toujours d’un seul tenant, depuis une adresse de départ avec une longueur donnée...
La conséquence de ce comportement est que l’usage abusif d’allocations/libérations va tellement fragmenter la mémoire qu’à un moment plus rien ne pourra être alloué, même si la somme de tous les petits bouts de mémoire libres dépasse ce que l’on veut allouer.
La morale est au maximum de ne pas abuser de cycles d’allocation / désallocation pendant le fonctionnement du programme. Un bon moyen de limiter le risque, c’est de faire toutes les allocations pendant le setup(), et de s’assurer que le loop() n’en fait plus...

La pile a t-elle une fuite ?

Une pile, dans le monde informatique, est une zone de mémoire à partir de laquelle on va empiler des données éventuellement hétérogènes.
Il y a deux types de piles : les FIFO (First In First Out / Premier entré, premier sorti) qui fonctionne comme une file d’attente chez le marchand : le plus ancien élément sort en premier, et les LIFO (Last In First Out / Dernier entré, premier sorti) qui fonctionne comme une pile d’assiette. On reprend toujours la dernière assiette posée, celle du sommet. C’est ce dernier cas qui va nous intéresser.

File:Lifo.png - Wikimedia Commons
File:Lifo.png - Wikimedia Commons

A quoi sert la pile ?

Le principal besoin de mémoire du processeur lui même pendant l’exécution d’un programme se passe dans l’enchaînement des fonctions.

  • Pour le processeur, l’instruction en cours d’exécution est connue par le pointeur d’exécution. Ce pointeur est unique pour un processus, une exécution. Si le processeur veut exécuter une autre commande, il doit déplacer ce pointeur, donc changer sa valeur. Cela signifie que si une fonction est appelée, la valeur courante du pointeur d’exécution doit être stockée quelque part pour que le processeur puisse y revenir lorsque la fonction sera terminée... Et c’est évidemment la pile qui sert de conteneur pour ces mémorisations.

Première conséquence : on voit bien aussi que multiplier les toutes petites fonctions qui s’appellent mutuellement en cascade va nécessiter beaucoup de place de stockage des pointeurs de retour...

  • Dans une fonction, vous avez souvent des variables locales. Ces variables occupent forcément de la mémoire puisqu’elles ont un contenu... Gagné, c’est aussi sur la pile !

Deuxième conséquence : limiter le volume et la taille des variables locales va forcément diminuer le besoin de la pile...

  • Ce n’est pas tout. Lorsqu’une fonction est appelée, des arguments sont souvent présents. Tous les processeurs disposent d’emplacements mémoire spécialisés appelés registres qui sont d’un accès très rapide puisque câblés directement dans le silicium du processeur et utilisés prioritairement. Sur les Atmel/Avr, ces registres reçoivent les arguments passés à la fonction, mais s’ils sont trop nombreux ou de types indéfinis à la compilation, comme pour des listes d’arguments variables (varargs) , c’est encore la pile qui est utilisée.

Troisième conséquence : diminuer la taille des arguments de fonction peut aussi diminuer la taille de la pile...

Par nature, la taille de la pile dépend du moment où on la mesure. Si vous êtes dans le Setup, hormis les variables internes à cette fonction, la pile sera quasiment vide. Mais si vous vous trouvez au fin fond d’une sous-sous-sous-sous fonction, elle sera bien plus importante, pour redevenir quasi vide dès que vous en serez sorti ! Sa taille n’est écrite nulle part, et le processeur prendra la place dont il a besoin sans vérifier que cela écrase quelque chose...

Pour éviter de collisionner avec les autres utilisateurs de la SRAM, la pile commence de la fin de la SRAM et descend vers le début ! Evidemment, si sa taille devient trop importante, elle risque de chevaucher la zone des allocations, sa gestion va en écraser la fin, et inversement le remplissage de cette zone d’allocation va écraser le contenu de la pile...

PNG - 9 kio

Que ce soit le processeur pendant l’exécution, ou le compilateur sur l’ordinateur, personne ne signalera ce problème. Simplement vous aurez des plantages intempestifs si un pointeur d’exécution est écrasé, des fonctionnements erratiques si des variables locales changent de valeur ou le contenu d’une mémoire allouée change brutalement et sans raison ou qu’une allocation échoue faute de place... Aucun outil de contrôle ne vous signalera le problème !

C’est là que SuperBibliothèque MemoryUsage apparaît !

La bibliothèque propose de vous montrer l’état de votre mémoire au moment de la demande. Quelques macros sont là pour afficher les différents pointeurs :

  • MEMORY_PRINT_START : le départ de la mémoire dynamique SRAM. Elle peut ne pas commencer à zéro !
  • MEMORY_PRINT_HEAPSTART le départ de la zone disponible pour les allocations, immédiatement après les variables globales signalées par le compilateur (les 182 octets cités plus haut...).
  • MEMORY_PRINT_HEAPEND la fin actuelle de la zone des allocations.
  • MEMORY_PRINT_STACKSTART le début de la pile.
  • MEMORY_PRINT_END la fin de la SRAM, qui est aussi la fin de la pile.
  • MEMORY_PRINT_HEAPSIZE La taille de la mémoire allouée par des new ou des alloc.
  • MEMORY_PRINT_STACKSIZE La taille de la pile à ce moment
  • MEMORY_PRINT_FREERAM Le volume de mémoire SRAM encore disponible.
  • MEMORY_PRINT_TOTALSIZE La taille mémoire disponible totale
PNG - 10.2 kio

La fonction SRamDisplay() va directement afficher le graphique chiffré de la situation de la mémoire sur la console, mais sans représenter la fragmentation éventuelle de la zone d’allocation.

PNG - 7.3 kio

mu_FreeRam() va donner la taille mémoire disponible au moment de la demande, c’est à dire le delta entre heap_end, la fin de la zone d’allocation et stack_start, le bas de la pile.

La vie de la pile...

Une pile, c’est vivant. Elle change sans cesse, diminue, augmente, puis diminue encore au rythme des appels de fonction. Et un peu comme dans la physique quantique, l’observer change l’observation... Malgré tout comment faire pour tenter de deviner sa taille maximum ? Dans MemoryUsage, deux approches complémentaires, issues de mes recherches sur le Web ont été utilisées.

Le Renoir de la pile ?

La première méthode disponible est subtile. Dès que vous incluez la bibliothèque MemoryUsage, une fonction d’initialisation est utilisée par le processeur avant même le lancement du Setup(), c’est mu_StackPaint(). Cette fonction, que vous ne devez pas appeler, va ’peindre’ (ou plutôt remplir) toute la mémoire disponible à ce moment là avec un caractère particulier. Il suffit ensuite à n’importe quel moment d’appeler la fonction mu_StackCount() pour compter les occurrences de ce caractère pour savoir jusqu’où a été utilisée la pile... C’est un peu comme si on avait peint toute la mémoire en jaune canari, et qu’après usage, on regarde jusqu’où vont les traces de pas des utilisateurs !

Échantillonner...

Le second moyen pour traquer la taille maxi de la pile consiste à la mesurer le plus souvent possible et à en retenir la plus grande valeur. C’est assez laid parce que cela oblige à faire de nombreuses modifications dans son code :

  • STACK_DECLARE comme son nom l’indique va créer la variable qui va contenir la plus grande valeur de pile atteinte. Il doit être placé au début du fichier .ino, juste après les include et surtout en dehors de toute fonction :
#include <MemoryUsage.h>

STACK_DECLARE

void setup() 
....
  • STACK_COMPUTE doit être appelé à l’entrée de chaque fonction pour mettre à jour la variable globale au fur et à mesure...
void subFonction()
{
    double v[SIZE];
    STACK_COMPUTE;

    .... // fait des choses...
}

void loop() 
{
    subFonction();
}

Lorsque l’on estime que le programme a assez travaillé pour avoir une taille de pile réaliste, par exemple à la fin du setup(), et à chaque pas de boucle de loop(), on peut alors l’afficher sur la console avec STACK_PRINT avec un texte standard suivi de la valeur, ou STACK_PRINT_TEXT si on veut personnaliser ce texte.

Pour finir ce tour d’horizon des méthodes de traque des octets, n’oublions pas comme je l’ai dit plus haut que l’appel à des fonctions de calcul de la pile ou d’affichage va forcément faire grossir la pile et la mémoire programme, ralentir un petit peu l’exécution des fonctions et polluer la console série !

Bibliothèque disponible ici : Forge Locoduino

Notez que dans le répertoire ’extras/Doc’ se trouve une documentation html visible sur n’importe quel système avec n’importe quel navigateur un peu moderne. Cette documentation reprend en Anglais l’ensemble des informations données ici et détaille plus précisément les rôles des classes, des fonctions et de leurs arguments. Sous Windows pour la lancer, il suffit de double cliquer sur le fichier StartDoc.bat présent dans le répertoire de la bibliothèque, sinon il faut manuellement double-cliquer sur le fichier ’extras/Doc/index.html’ .

Contrôler les tailles mémoire reste un travail ingrat, fait d’essais divers, de tentatives plus ou moins fructueuses d’apporter des modifications pour améliorer les choses, et d’affichage de hiéroglyphes et de textes abscons sur la console... Mais quel plaisir de se rendre compte que l’on a gagné deux octets de cette précieuse SRAM !
Dans un article ultérieur, nous parleront des moyens d’améliorer le bilan mémoire de votre programme au vu des statistiques données par MemoryUsage...

4 Messages

  • Bibliothèque MemoryUsage 17 mars 2021 00:30, par Andrew Mowry

    Hello,

    I am trying to solve an Arduino memory usage issue (Atmega 32u4) and I’m using your library. I notice that there’s a big difference between the results of MEMORY_PRINT_FREERAM and FREERAM_PRINT macros. The first one doesn’t seem to correspond to the rest of the results (shown below). Would you be able to tell me why this might be ? Thanks very much for your time !

    Free ram :-175 <<result from MEMORY_PRINT_FREERAM
    Free Ram Size : 1309 <<result from FREERAM_PRINT

    +----------------+ 256 (__data_start)
    + data +
    + variables + size = 562
    +----------------+ 818 (__data_end / __bss_start)
    + bss +
    + variables + size = 665
    +----------------+ 1483 (__bss_end / __heap_start)
    + heap + size = 0
    +----------------+ 1483 (__brkval if not 0, or __heap_start)
    + +
    + +
    + FREE RAM + size = 1308
    + +
    + +
    +----------------+ 2791 (SP)
    + stack + size = 25
    +----------------+ 2815 (RAMEND / __stack)

    Répondre

    • Bibliothèque MemoryUsage 17 mars 2021 21:19, par Thierry

      Hello.

      You are right, there was a bug solved in the version 2.21.0. You can get it on github, or ask for an update inside the library manager of the IDE.

      Répondre

  • Bibliothèque MemoryUsage 21 janvier 2022 22:50, par Sam

    Hi,
    I am working with Arduino Nano 33 BLE. Unfortunately, it looks like the library is not compatible with “mbed_nano” architecture.
    Does it make sense to you ?
    Do you know what would be the alternative for this type of board ?
    Thanks
    Sam

    Répondre

Réagissez à « Bibliothèque MemoryUsage »

Qui êtes-vous ?
Votre message

Pour créer des paragraphes, laissez simplement des lignes vides.

Lien hypertexte

(Si votre message se réfère à un article publié sur le Web, ou à une page fournissant plus d’informations, vous pouvez indiquer ci-après le titre de la page et son adresse.)

Rubrique « Bibliothèques »

Les derniers articles

Les articles les plus lus