Quelles sont les meilleures pratiques pour réduire l’utilisation de la mémoire en C?

Quelles sont les meilleures pratiques pour la “Programmation C économe en mémoire”. Surtout pour les appareils embarqués / mobiles, quelles devraient être les consignes en matière de consommation de mémoire réduite?

Je suppose qu’il devrait y avoir des directives distinctes pour a) la mémoire de code b) la mémoire de données

En C, à un niveau beaucoup plus simple, considérons ce qui suit:

  • Utilisez #pragma pack (1) pour aligner octet vos structures
  • Utiliser des unions où une structure peut contenir différents types de données
  • Utilisez des champs de bits plutôt que des entiers pour stocker les indicateurs et les petits entiers
  • Évitez d’utiliser des tableaux de caractères de longueur fixe pour stocker des chaînes, implémenter un pool de chaînes et utiliser des pointeurs.
  • Où stocker des références à une liste de chaînes énumérée, par exemple le nom de la police, stocke un index dans la liste plutôt que la chaîne
  • Lorsque vous utilisez l’allocation de mémoire dynamic, calculez le nombre d’éléments requirejs à l’avance pour éviter les reallocs.

Quelques suggestions que j’ai trouvées utiles pour travailler avec des systèmes embarqués:

  • Assurez-vous que toutes les tables de recherche ou autres données constantes sont réellement déclarées à l’aide de const . Si const est utilisé, les données peuvent être stockées dans une mémoire en lecture seule (par exemple, flash ou EEPROM), sinon les données doivent être copiées dans la RAM au démarrage, ce qui occupe à la fois des espaces flash et RAM. Définissez les options de l’éditeur de liens afin qu’il génère un fichier de carte et étudiez ce fichier pour voir exactement où vos données sont allouées dans la carte mémoire.

  • Assurez-vous que vous utilisez toutes les régions de mémoire disponibles. Par exemple, les microcontrôleurs ont souvent une mémoire intégrée que vous pouvez utiliser (dont l’access peut également être plus rapide que la RAM externe). Vous devriez être capable de contrôler les régions de mémoire auxquelles le code et les données sont alloués à l’aide des parameters d’options du compilateur et de l’éditeur de liens.

  • Pour réduire la taille du code, vérifiez les parameters d’optimisation de votre compilateur. La plupart des compilateurs ont des commutateurs pour optimiser la vitesse ou la taille du code. Il peut être intéressant de tester ces options pour voir si la taille du code compilé peut être réduite. Et évidemment, éliminez le code en double dans la mesure du possible.

  • Vérifiez la quantité de mémoire de stack dont votre système a besoin et ajustez l’allocation de mémoire de l’éditeur de liens en conséquence (voir les réponses à cette question ). Pour réduire l’utilisation de la stack, évitez de placer de grandes structures de données sur la stack (quelle que soit la valeur de “grande” qui vous concerne).

Assurez-vous d’utiliser des calculs à points fixes / nombres entiers autant que possible. De nombreux développeurs utilisent des mathématiques à virgule flottante (ainsi que des performances médiocres, des bibliothèques volumineuses et une utilisation de la mémoire) lorsque de simples calculs entiers mis à l’échelle suffisent.

Toutes les bonnes recommandations. Voici quelques approches de conception que j’ai trouvées utiles.

  • Codage d’octets

Ecrivez un interpréteur pour un jeu d’instructions à code octet spécial et écrivez autant de programme que possible dans ce jeu d’instructions. Si certaines opérations nécessitent des performances élevées, convertissez-les en code natif et appelez-les à partir de l’interpréteur.

  • Génération de code

Si une partie des données d’entrée change très peu, vous pouvez avoir un générateur de code externe qui crée un programme ad-hoc. Ce sera plus petit qu’un programme plus général, tout en fonctionnant plus rapidement et en évitant d’allouer de la mémoire pour les entrées qui changent rarement.

  • Être un hater les données

Soyez prêt à gaspiller beaucoup de cycles si cela vous permet de stocker une structure de données absolument minimale. Habituellement, vous constaterez que la performance en souffre très peu.

Évitez la fragmentation de la mémoire en utilisant votre propre allocateur de mémoire (ou avec précaution en utilisant l’allocateur du système).

Une méthode consiste à utiliser un “allocateur de dalle” (voir cet article, par exemple) et plusieurs pools de mémoire pour des objects de tailles différentes.

Pré-allouer toute la mémoire en amont (c’est-à-dire, aucun appel malloc, sauf pour l’initialisation au démarrage) est certainement utile pour l’utilisation de la mémoire déterministe. Sinon, différentes architectures fournissent des techniques pour vous aider. Par exemple, certains processeurs ARM fournissent un jeu d’instructions alternatif (Thumb) qui divise presque de moitié la taille du code en utilisant des instructions de 16 bits au lieu des 32 bits normaux. Bien sûr, la vitesse est sacrifiée pour le faire …

Très probablement, vous devrez choisir vos algorithmes avec soin. Visez les algorithmes qui ont une utilisation de mémoire O (1) ou O (log n) (c’est-à-dire faible). Par exemple, les tableaux redimensionnables en continu (par exemple, std::vector ) nécessitent dans la plupart des cas moins de mémoire que les listes chaînées.

Parfois, l’utilisation de tables de consultation peut être plus bénéfique à la fois pour la taille et la vitesse du code. Si vous avez seulement besoin de 64 entrées dans une table de correspondance, cela représente 16 * 4 octets pour sin / cos / tan (utilisez la symésortinge!) Par rapport à une fonction de grande taille sin / cos / tan.

La compression aide parfois. Des algorithmes simples tels que RLE sont faciles à compresser / décompresser lorsqu’ils sont lus séquentiellement.

Si vous avez affaire à des graphiques ou à de l’audio, envisagez différents formats. Les graphiques palettisés ou bitpackés * peuvent constituer un bon compromis en termes de qualité. Les palettes peuvent être partagées entre de nombreuses images, ce qui réduit encore la taille des données. Le son peut être réduit de 16 bits à 8 bits ou même 4 bits, et la stéréo peut être convertie en mono. Les taux d’échantillonnage peuvent être réduits de 44,1 kHz à 22 kHz ou 11 kHz. Ces transformations audio réduisent considérablement la taille des données (et, malheureusement, la qualité) et sont sortingviales (à l’exception du rééchantillonnage, mais c’est à cela que servent les logiciels audio =]).

* Je suppose que vous pourriez mettre cela sous compression. Le bitpacking pour les graphiques consiste généralement à réduire le nombre de bits par canal afin que chaque pixel puisse tenir dans deux octets (RGB565 ou ARGB155 par exemple) ou un (ARGB232 ou RGB332) des trois ou quatre originaux (RGB888 ou ARGB8888, respectivement).

Certaines opérations d’parsing peuvent être effectuées sur les stream lorsque les octets arrivent plutôt que de les copier dans le tampon et de les parsingr.

Quelques exemples de ceci:

  1. Analyser une vapeur NMEA avec une machine à états, en ne collectant que les champs requirejs dans une structure beaucoup plus efficace.
  2. Analyser XML en utilisant SAX au lieu de DOM.

1) Avant de commencer le projet, créez un moyen de mesurer la quantité de mémoire que vous utilisez, de préférence par composant. Ainsi, chaque fois que vous effectuez un changement, vous pouvez voir ses effets sur l’utilisation de la mémoire. Vous ne pouvez pas optimiser ce que vous ne pouvez pas mesurer.

2) Si le projet est déjà mature et respecte les limites de la mémoire (ou s’il est porté sur un périphérique avec moins de mémoire), déterminez pour quoi vous utilisez déjà la mémoire.

D’après mon expérience, presque toute l’optimisation significative de la correction d’une application surdimensionnée provient d’un petit nombre de modifications: réduction de la taille du cache, suppression de certaines textures (bien entendu, il s’agit d’un changement fonctionnel qui nécessite l’accord des parties prenantes, à savoir des réunions, etc. ne soyez pas efficace en termes de temps), rééchantillonnez l’audio, réduisez la taille initiale des tas alloués sur mesure, trouvez des moyens de libérer des ressources uniquement utilisées temporairement et rechargez-les si nécessaire. Parfois, vous trouverez une structure de 64 octets qui pourrait être réduite à 16 bits ou autre, mais c’est rarement le fruit le plus bas. Si vous savez quelles sont les plus grandes listes et les plus grandes baies de l’application, alors vous savez quelles structures regarder en premier.

Oh oui: trouvez et corrigez les memory leaks. Toute mémoire que vous pouvez récupérer sans sacrifier les performances est un bon début.

J’ai passé beaucoup de temps dans le passé à me préoccuper de la taille du code. Les principales considérations (à part: assurez-vous de le mesurer au moment de la construction afin de pouvoir le voir changer), sont les suivantes:

1) Découvrez quel code est référencé et par quoi. Si vous découvrez qu’une bibliothèque XML entière est liée à votre application uniquement pour parsingr un fichier de configuration à deux éléments, envisagez de modifier le format du fichier de configuration et / ou d’écrire votre propre parsingur syntaxique sortingvial. Si vous le pouvez, utilisez une parsing source ou une parsing binary pour tracer un graphique de dépendance volumineux et recherchez des composants volumineux composés d’un petit nombre d’utilisateurs: il peut être possible de les extraire avec uniquement des réécritures de code mineures. Préparez-vous à jouer à diplomate: si deux composants différents de votre application utilisent XML et que vous souhaitez le réduire, vous devez convaincre deux personnes des avantages de la réalisation manuelle d’un élément de bibliothèque fiable et prêt à l’emploi. .

2) Déroulez avec les options du compilateur. Consultez la documentation spécifique à votre plate-forme. Par exemple, vous pouvez vouloir réduire l’augmentation de la taille de code acceptable par défaut en raison de l’inline, et au moins sur GCC, vous pouvez indiquer au compilateur uniquement d’appliquer des optimisations qui n’augmentent généralement pas la taille du code.

3) Tirez parti des bibliothèques déjà présentes sur la ou les plates-formes cibles dans la mesure du possible, même si cela implique l’écriture d’une couche d’adaptateur. Dans l’exemple XML ci-dessus, vous pouvez constater que sur votre plate-forme cible, il y a toujours une bibliothèque XML en mémoire, car le système d’exploitation l’utilise, auquel cas elle est liée dynamicment.

4) Comme quelqu’un d’autre l’a mentionné, le mode pouce peut aider sur ARM. Si vous ne l’utilisez que pour le code qui n’est pas critique en termes de performances et laissez les routines critiques dans ARM, vous ne remarquerez pas la différence.

Enfin, il est possible que vous puissiez jouer à des astuces intelligentes si vous avez un contrôle suffisant sur l’appareil. L’interface utilisateur ne permet qu’une seule application à exécuter à la fois? Déchargez tous les pilotes et services dont votre application n’a pas besoin. L’écran est en double tampon, mais votre application est synchronisée avec le cycle de rafraîchissement? Vous pourrez peut-être récupérer un tampon d’écran entier.

Recommander ce livre Small Memory Software: Patterns pour les systèmes à mémoire limitée

  • Réduisez la longueur et éliminez autant de constantes de chaîne que possible pour réduire l’espace de code

  • Examinez attentivement les compromis entre les algorithmes et les tables de consultation, le cas échéant

  • Soyez conscient de la façon dont différents types de variables sont alloués.

    • Les constantes sont probablement dans l’espace de code.
    • Les variables statiques sont probablement à des emplacements de mémoire fixes. Évitez ces si possible.
    • Les parameters sont probablement stockés sur la stack ou dans des registres.
    • Les variables locales peuvent également être allouées à partir de la stack. Ne déclarez pas de grands tableaux ou chaînes locaux si le code risque de manquer d’espace dans la stack dans les pires conditions.
    • Vous n’avez peut-être pas de tas – il n’existera peut-être pas de système d’exploitation pour gérer le tas pour vous. Est-ce acceptable? Avez-vous besoin d’une fonction malloc ()?

Une astuce utile dans les applications consiste à créer un fonds de mémoire pluvieux. Allouez au démarrage un seul bloc suffisamment volumineux pour suffire aux tâches de nettoyage. Si malloc / new échoue, libérez le fonds Rainy Day et publiez un message indiquant aux utilisateurs que les ressources sont limitées et qu’ils doivent économiser rapidement. Cette technique était utilisée dans de nombreuses applications Mac vers 1990.

Un excellent moyen de limiter les besoins en mémoire consiste à utiliser autant que possible la libc ou d’autres bibliothèques standard pouvant être liées de manière dynamic. Chaque DLL ou object partagé supplémentaire que vous devez inclure dans votre projet constitue un bloc de mémoire important que vous pourrez peut-être éviter de graver.

De plus, utilisez les unions et les champs de bits, le cas échéant, ne chargez que la partie de vos données sur laquelle votre programme travaille, et assurez-vous que vous comstackz avec le commutateur -Os (dans gcc; ou son équivalent du compilateur) optimiser pour la taille du programme.

J’ai une présentation de la conférence sur les systèmes embarqués disponible sur ce sujet. C’est à partir de 2001, mais c’est quand même très applicable. Voir le papier .

De même, si vous pouvez choisir l’architecture du périphérique cible, optez pour un ARM moderne avec Thumb V2, un PowerPC avec VLE ou MIPS16, ou sélectionnez des cibles compactes connues telles que Infineon TriCore ou la famille SH. très bonne option. Sans parler de la famille NEC V850E qui est bien compacte. Ou passez à un AVR qui a une excellente compacité du code (mais est une machine 8 bits). Tout sauf un RISC 32 bits de longueur fixe est un bon choix!

Outre les suggestions fournies par d’autres utilisateurs, n’oubliez pas que les variables locales déclarées dans vos fonctions seront normalement allouées sur la stack.

Si la mémoire de stack est limitée ou si vous souhaitez réduire la taille de la stack afin de libérer de la place pour davantage de mémoire vive ou globale, prenez en compte les éléments suivants:

  1. Aplatissez votre arborescence d’appels pour réduire le nombre de variables sur la stack à un moment donné.
  2. Convertissez des variables locales volumineuses en variables globales (réduit la quantité de stack utilisée, mais augmente la quantité de RAM globale utilisée). Les variables peuvent être déclarées:

    • Portée globale: visible pour l’ensemble du programme
    • Statique à la scope du fichier: visible dans le même fichier
    • Static at scope de la fonction: visible dans la fonction
    • REMARQUE: peu importe si ces modifications sont apscopes, vous devez vous méfier des problèmes de code reentrant si vous avez un environnement preemptive .

De nombreux systèmes intégrés ne disposent pas d’un diagnostic de moniteur de stack pour s’assurer qu’un débordement de stack est détecté. Une parsing est donc nécessaire.

PS: Bonus pour une utilisation appropriée de Stack Overflow ?