Comment compresser le code pour plus de Flash et de RAM? [fermé]

14

J'ai travaillé sur le développement d'une fonctionnalité sur un de nos produits particuliers. Il y a eu une demande de portage de la même fonctionnalité vers un autre produit. Ce produit est basé sur un microcontrôleur M16C, qui a traditionnellement 64K Flash et 2k de RAM.

C'est un produit mature, et il ne lui reste donc que 132 octets de Flash et 2 octets de RAM.

Pour porter la fonctionnalité demandée (la fonctionnalité elle-même a été optimisée), j'ai besoin de 1400 octets de Flash et ~ 200 octets de RAM.

Quelqu'un at-il des suggestions sur la façon de récupérer ces octets par compactage de code? Quelles sont les choses spécifiques que je recherche lorsque j'essaie de compacter du code de travail déjà existant?

Toutes les idées seront vraiment appréciées.

Merci.

IntelliChick
la source
1
Merci à tous pour les suggestions. Je vous tiendrai au courant de mes progrès et énumérerai les étapes qui ont fonctionné et celles qui n'ont pas fonctionné.
IntelliChick
Ok, voici les choses que j'ai essayées et qui ont fonctionné: Versions de compilateur déplacées. L'optimisation s'était considérablement améliorée, ce qui m'a donné environ 2 Ko de Flash. A parcouru les fichiers de liste pour vérifier les fonctionnalités redondantes et inutilisées (héritées en raison de la base de code commune) pour le produit particulier et a gagné un peu plus de Flash.
IntelliChick
Pour la RAM, j'ai fait ce qui suit: J'ai parcouru le fichier de carte pour vérifier les fonctions / modules qui utilisaient le plus de RAM. J'ai trouvé une fonction vraiment lourde (12 canaux, chacun avec une quantité fixe de mémoire allouée), du code hérité, compris ce qu'il essayait d'atteindre et optimisé l'utilisation de la RAM, en partageant des informations entre les canaux qui étaient communs. Cela m'a donné ~ 200 octets dont j'avais besoin.
IntelliChick
Si vous avez des fichiers ascii, vous pouvez utiliser une compression de 8 à 7 bits. Vous fait économiser 12,5%. L'utilisation d'un fichier zip nécessiterait plus de code pour le compresser et le décompresser que de le laisser simplement.
Sparky256

Réponses:

18

Vous avez deux options: la première consiste à rechercher du code redondant et à le déplacer vers un seul appel pour éliminer la duplication; la seconde consiste à supprimer la fonctionnalité.

Examinez bien votre fichier .map et voyez s'il existe des fonctions dont vous pouvez vous débarrasser ou réécrire. Assurez-vous également que les appels de bibliothèque utilisés sont vraiment nécessaires.

Certaines choses comme la division et les multiplications peuvent apporter beaucoup de code, mais l'utilisation des décalages et une meilleure utilisation des constantes peuvent rendre le code plus petit. Jetez également un oeil à des choses comme les constantes de chaîne et printfs. Par exemple chacunprintf mangera votre rom, mais vous pourrez peut-être avoir quelques chaînes de format partagé au lieu de répéter cette chaîne à plusieurs reprises.

Pour la mémoire, voyez si vous pouvez vous débarrasser des globaux et utiliser des autos dans une fonction à la place. Évitez également autant de variables que possible dans la fonction principale, car elles consomment de la mémoire comme le font les globaux.

Rex Logan
la source
1
Merci pour les suggestions, je peux certainement essayer la plupart d'entre elles, sauf celle pour les constantes de chaîne. Il s'agit uniquement d'un périphérique intégré, sans interface utilisateur et il n'y a donc aucun appel à printf () dans le code. En espérant que ces suggestions me remueront pour obtenir mon 1400 octets Flash / 200 octets RAM dont j'ai besoin.
IntelliChick
1
@IntelliChick, vous seriez étonné du nombre de personnes qui utilisent printf () à l'intérieur d'un périphérique intégré pour imprimer pour le débogage ou l'envoi vers un périphérique. Il semble que vous le sachiez mieux que cela, mais si quelqu'un a écrit du code sur le projet avant vous, cela ne ferait pas de mal de le vérifier.
Kellenjb
5
Et juste pour développer mon commentaire précédent, vous seriez également étonné du nombre de personnes qui ajoutent des instructions de débogage, mais ne les suppriment jamais. Même les gens qui font #ifdefs deviennent parfois paresseux.
Kellenjb
1
Génial merci! J'ai hérité de cette base de code, donc je vais certainement les chercher. Je vais vous tenir au courant des progrès et essayer de garder une trace du nombre d'octets de mémoire ou de Flash que j'ai gagné en faisant quoi, juste comme référence pour toute autre personne qui pourrait avoir besoin de le faire à l'avenir.
IntelliChick
Juste une question à ce sujet - qu'en est-il des sauts d'appel de fonction imbriquée de couche en couche. Combien de frais généraux cela ajoute-t-il? Est-il préférable de conserver la modularité en ayant plusieurs appels de fonction ou de réduire les appels de fonction, et d'économiser quelques octets. Et est-ce important?
IntelliChick
8

Il vaut toujours la peine de consulter la sortie du fichier (assembleur) pour rechercher les choses sur lesquelles votre compilateur particulier est particulièrement mauvais.

Par exemple, vous pouvez constater que les variables locales sont très chères, et si l'application est suffisamment simple pour en valoir le risque, le déplacement de quelques compteurs de boucles dans des variables statiques peut économiser beaucoup de code.

Ou l'indexation de tableaux peut être très coûteuse mais les opérations de pointeur beaucoup moins chères. Ou vice versa.

Mais regarder le langage d'assemblage est la première étape.


la source
3
Il est très important que vous sachiez ce que fait votre compilateur. Vous devriez voir ce qui divise sur mon compilateur. Ça fait pleurer les bébés (moi y compris).
Kortuk
8

Les optimisations du compilateur, par exemple, -Osdans GCC donnent le meilleur équilibre entre la vitesse et la taille du code. À éviter -O3, car cela peut augmenter la taille du code.

Thomas O
la source
3
Si vous faites cela, vous devrez retester TOUT! Les optimisations peuvent entraîner le non-fonctionnement du code de travail en raison des nouvelles hypothèses émises par le compilateur.
Robert
@Robert, cela n'est vrai que si vous utilisez des instructions non définies: par exemple, a = a ++ se compilera différemment dans -O0 et -O3.
Thomas O
5
@Thomas n'est pas vrai. Si vous avez une boucle for pour retarder les cycles d'horloge, de nombreux optimiseurs se rendront compte que vous n'y faites rien et la supprimeront. Ce n'est qu'un exemple.
Kellenjb
1
@thomas O, Vous devez également vous assurer que vous faites attention aux définitions des fonctions volatiles. Les optimiseurs feront exploser ceux qui pensent connaître bien le C mais qui ne comprennent pas la complexité des opérations atomiques.
Kortuk
1
Tous les bons points. Les fonctions / variables volatiles, par définition, ne doivent PAS être optimisées. Tout optimiseur qui effectue des optimisations sur ceux-ci (y compris le temps d'appel et l'inline) est cassé.
Thomas O
8

Pour la RAM, vérifiez la plage de toutes vos variables - utilisez-vous des ints où vous pourriez utiliser un caractère? Les tampons sont-ils plus grands que nécessaire?

La compression de code est très dépendante de l'application et du style de codage. Vos montants restants suggèrent que le code a peut-être déjà disparu malgré une compression, ce qui peut signifier qu'il reste peu de choses à avoir.

Jetez également un coup d'œil à la fonctionnalité globale - y a-t-il quelque chose qui n'est pas vraiment utilisé et qui peut être abandonné?

mikeselectricstuff
la source
8

Si c'est un vieux projet mais que le compilateur a été développé depuis, il se peut qu'un compilateur plus récent puisse produire un code plus petit

mikeselectricstuff
la source
Merci Mike! J'ai déjà essayé par le passé et l'espace gagné a déjà été utilisé! :) Déplacé du compilateur IAR C 3.21d à 3.40.
IntelliChick
1
J'ai monté une version de plus et j'ai réussi à obtenir un peu plus de Flash pour intégrer la fonctionnalité. Cependant, je lutte vraiment avec la RAM, qui est restée inchangée. :(
IntelliChick
7

Il vaut toujours la peine de consulter le manuel de votre compilateur pour les options permettant d'optimiser l'espace.

Pour gcc -ffunction-sectionset -fdata-sectionsavec le--gc-sections drapeau de éditeur de liens sont bons pour supprimer le code mort.

Voici d'autres excellents conseils (axés sur l'AVR)

Toby Jaffey
la source
Est-ce que cela fonctionne réellement? Les documents indiquent "Lorsque vous spécifiez ces options, l'assembleur et l'éditeur de liens créeront des objets et des fichiers exécutables plus volumineux et seront également plus lents". Je comprends que le fait d'avoir des sections séparées est logique pour un micro avec des sections Flash et RAM - Cette déclaration dans les documents n'est-elle pas applicable aux microcontrôleurs?
Kevin Vermeer
Mon expérience est que cela fonctionne bien pour AVR
Toby Jaffey
1
Cela ne fonctionne pas bien dans la plupart des compilateurs que j'ai utilisés. C'est comme utiliser le mot-clé register. Vous pouvez dire au compilateur qu'une variable va dans un registre, mais un bon optimiseur le fera beaucoup mieux qu'un humain (soutenez que certains le peuvent, il est considéré comme inacceptable de le faire dans la pratique).
Kortuk
1
Lorsque vous commencez à attribuer des emplacements, vous forcez le compilateur à placer des éléments à certains emplacements, très important pour le code du chargeur de démarrage avancé, mais horrible à gérer dans l'optimiseur, lorsque vous prenez des décisions à ce sujet, vous enlevez une étape d'optimisation. pourrait faire. Dans certains compilateurs, ils le conçoivent pour avoir des sections pour quel code est utilisé, il s'agit de dire au compilateur plus d'informations pour comprendre votre utilisation, cela vous aidera. Si le compilateur ne le suggère pas, ne le faites pas.
Kortuk
6

Vous pouvez examiner la quantité d'espace de pile et d'espace de tas alloués. Vous pourrez peut-être récupérer une quantité importante de RAM si l'une ou l'autre ou les deux sont surallouées.

Je suppose pour un projet qui entre dans 2k de RAM pour commencer il n'y a pas d' allocation de mémoire dynamique (utilisation malloc, callocetc.). Si tel est le cas, vous pouvez vous débarrasser complètement de votre tas en supposant que l'auteur d'origine a laissé de la RAM allouée pour le tas.

Vous devez être très prudent en réduisant la taille de la pile car cela peut provoquer des bogues très difficiles à trouver. Il peut être utile de commencer par initialiser tout l'espace de pile à une valeur connue (autre chose que 0x00 ou 0xff car ces valeurs se produisent déjà couramment), puis d'exécuter le système pendant un certain temps pour voir combien d'espace de pile n'est pas utilisé.

semaj
la source
Ce sont de très bons choix. Je noterai que vous ne devriez jamais utiliser malloc dans un système embarqué.
Kortuk
1
@Kortuk Cela dépend de votre définition de l'embarqué et de la tâche exécutée
Toby Jaffey
1
@joby, oui, je comprends cela. Dans un système avec 0 redémarrage et l'absence d'un OS comme Linux, Malloc peut être très très mauvais.
Kortuk
Il n'y a pas d'allocation dynamique de mémoire, aucun endroit où malloc, calloc est utilisé. J'ai également vérifié l'allocation de tas, et elle a déjà été définie sur 0, il n'y a donc pas d'allocation de tas. La taille de pile actuellement allouée est de 254 octets et la taille de pile d'interruption de 128 octets.
IntelliChick
5

Votre code utilise-t-il des mathématiques à virgule flottante? Vous pourrez peut-être réimplémenter vos algorithmes en utilisant uniquement les mathématiques entières et éliminer les frais généraux liés à l'utilisation de la bibliothèque de virgule flottante C. Par exemple, dans certaines applications, des fonctions telles que sinus, log, exp peuvent être remplacées par des approximations polynomiales entières.

Votre code utilise-t-il de grandes tables de recherche pour des algorithmes tels que les calculs CRC? Vous pouvez essayer de remplacer une version différente de l'algorithme qui calcule les valeurs à la volée, au lieu d'utiliser les tables de recherche. La mise en garde est que le plus petit algorithme est probablement plus lent, alors assurez-vous d'avoir suffisamment de cycles CPU.

Votre code contient-il de grandes quantités de données constantes, telles que des tableaux de chaînes, des pages HTML ou des graphiques en pixels (icônes)? S'il est suffisamment volumineux (disons 10 Ko), il pourrait être utile de mettre en œuvre un schéma de compression très simple pour réduire les données et les décompresser à la volée si nécessaire.

Craig McQueen
la source
Il y a 2 petites tables de recherche, dont aucune ne s'élèvera malheureusement à 10K. Et aucun calcul à virgule flottante n'est utilisé non plus. :( Merci pour les suggestions cependant. Elles sont bonnes.
IntelliChick
2

Vous pouvez essayer de réorganiser pour coder beaucoup, dans un style plus compact. Cela dépend beaucoup de ce que fait le code. La clé est de trouver des choses similaires et de les réimplémenter les unes par rapport aux autres. Un extrême serait d'utiliser un langage de niveau supérieur, comme Forth, avec lequel il peut être plus facile d'atteindre une densité de code plus élevée qu'en C ou en assembleur.

Voici Forth pour M16C .

Rupture du contrat avec le professeur Falken
la source
2

Définissez le niveau d'optimisation du compilateur. De nombreux IDE ont des paramètres qui permettent des optimisations de la taille du code au détriment du temps de compilation (ou peut-être même du temps de traitement dans certains cas). Ils peuvent effectuer le compactage de code en réexécutant leur optimiseur plusieurs fois, en recherchant des modèles optimisables moins courants et une multitude d'autres astuces qui peuvent ne pas être nécessaires pour la compilation occasionnelle / de débogage. Habituellement, par défaut, les compilateurs sont définis sur un niveau moyen d'optimisation. Creusez dans les paramètres et vous devriez pouvoir trouver une échelle d'optimisation basée sur des nombres entiers.

Joel B
la source
1
Actuellement optimisé au maximum pour la taille. :) Merci pour la suggestion. :)
IntelliChick
2

Si vous utilisez déjà un compilateur de niveau professionnel comme IAR, je pense que vous aurez du mal à faire de sérieuses économies en modifiant légèrement le code de bas niveau - vous devrez chercher davantage à supprimer des fonctionnalités ou à effectuer des tâches majeures. réécrit les pièces de manière plus efficace. Vous devrez être un codeur plus intelligent que celui qui a écrit la version originale ... En ce qui concerne la RAM, vous devez examiner très attentivement comment elle est actuellement utilisée et voir s'il est possible de superposer l'utilisation de la même RAM pour différentes choses à différents moments (les syndicats sont utiles pour cela). Les tailles de tas et de piles par défaut d'IAR dans celles ARM / AVR que j'ai eu tendance à être trop généreuses, donc ce serait la première chose à regarder.

mikeselectricstuff
la source
Merci Mike. Le code utilise déjà des syndicats dans la plupart des endroits, mais j'examinerai d'autres endroits, où cela pourrait encore aider. Je vais également jeter un œil à la taille de pile choisie et voir si cela peut être optimisé du tout.
IntelliChick
Comment savoir quelle taille de pile est appropriée?
IntelliChick
2

Autre chose à vérifier - certains compilateurs sur certaines architectures copient les constantes dans la RAM - généralement utilisés lorsque l'accès aux constantes flash est lent / difficile (par exemple AVR). Par exemple, le compilateur AVR d'IAR nécessite un qualificatif _ _flash pour ne pas copier une constante dans la RAM)

mikeselectricstuff
la source
Merci Mike. Oui, j'avais déjà vérifié cela - c'est ce qu'on appelle l'option «Constantes inscriptibles» pour le compilateur M16C IAR C. Il copie les constantes de la ROM vers la RAM. Cette option n'est pas cochée pour mon projet. Mais un chèque vraiment valable! Merci.
IntelliChick
1

Si votre processeur ne prend pas en charge le matériel pour une pile de paramètres / locale mais que le compilateur essaie de toute façon d'implémenter une pile de paramètres d'exécution, et si votre code n'a pas besoin d'être rentré, vous pourrez peut-être enregistrer le code l'espace en allouant statiquement des variables automatiques. Dans certains cas, cela doit être fait manuellement; dans d'autres cas, les directives du compilateur peuvent le faire. Une allocation manuelle efficace nécessitera le partage de variables entre les routines. Un tel partage doit être fait avec soin, pour garantir qu'aucune routine n'utilise une variable qu'une autre routine considère comme "dans la portée", mais dans certains cas, les avantages de la taille du code peuvent être importants.

Certains processeurs ont des conventions d'appel qui peuvent rendre certains styles de passage de paramètres plus efficaces que d'autres. Par exemple, sur les contrôleurs PIC18, si une routine prend un seul paramètre d'un octet, il peut être passé dans un registre; si cela prend plus que cela, tous les paramètres doivent être passés en RAM. Si une routine prend deux paramètres d'un octet, il peut être plus efficace de "passer" l'un dans une variable globale, puis de passer l'autre comme paramètre. Avec des routines largement utilisées, les économies peuvent s'additionner. Ils peuvent être particulièrement importants si le paramètre transmis via global est un indicateur à bit unique, ou s'il aura généralement une valeur de 0 ou 255 (car des instructions spéciales existent pour stocker un 0 ou 255 dans la RAM).

Sur l'ARM, le fait de mettre des variables globales fréquemment utilisées ensemble dans une structure peut réduire considérablement la taille du code et améliorer les performances. Si A, B, C, D et E sont des variables globales distinctes, le code qui les utilise toutes doit charger l'adresse de chacune dans un registre; s'il n'y a pas suffisamment de registres, il peut être nécessaire de recharger ces adresses plusieurs fois. En revanche, s'ils font partie de la même structure globale MyStuff, le code qui utilise MyStuff.A, MyStuff.B, etc. peut simplement charger une fois l'adresse de MyStuff. Grande victoire.

supercat
la source
1

1.Si votre code dépend de nombreuses structures, assurez-vous que les membres de la structure sont classés de ceux qui occupent le plus de mémoire au moins.

Ex: "uint32_t uint16_t uint8_t" au lieu de "uint16_t uint8_t uint32_t"

Cela garantira un rembourrage de structure minimum.

2.Utilisez const pour les variables, le cas échéant. Cela garantira que ces variables seront en ROM et ne consommeront pas de RAM

AkshayImmanuelD
la source
1

Quelques astuces (peut-être évidentes) que j'ai utilisées avec succès pour compresser le code d'un client:

  1. Drapeaux compacts en champs de bits ou masques de bits. Cela peut être bénéfique, car les booléens sont généralement stockés sous forme d'entiers, ce qui gaspille de la mémoire. Cela permettra d'économiser à la fois de la RAM et de la ROM et n'est généralement pas effectué par le compilateur.

  2. Recherchez la redondance dans le code et utilisez des boucles ou des fonctions pour exécuter des instructions répétées.

  3. J'ai également économisé de la ROM en remplaçant de nombreuses if(x==enum_entry) <assignment>instructions de constantes par un tableau indexé, en prenant soin que les entrées enum puissent être utilisées comme index de tableau

clabacchio
la source
0

Si vous le pouvez, utilisez des fonctions en ligne ou des macros de compilation au lieu de petites fonctions. Il y a des frais généraux de taille et de vitesse avec des arguments qui passent et qui peuvent être corrigés en rendant la fonction en ligne.

AngryEE
la source
1
Tout compilateur décent devrait le faire automatiquement pour les fonctions appelées une seule fois.
mikeselectricstuff
5
J'ai trouvé que l'inline est généralement plus utile pour les optimisations de vitesse, et généralement au prix d'une taille accrue.
Craig McQueen
l'incrustation augmente généralement la taille du code, sauf avec des fonctions triviales commeint get_a(struct x) {return x.a;}
Dmitry Grigoryev
0

Modifiez les variables locales pour qu'elles aient la même taille que vos registres CPU.

Si le processeur est de 32 bits, utilisez des variables de 32 bits même si la valeur maximale ne dépassera jamais 255. Si vous avez utilisé une variable de 8 bits, le compilateur ajoutera du code pour masquer les 24 bits supérieurs.

La première place que je regarderais est celle des variables for-loop.

for( i = 0; i < 100; i++ )

Cela peut sembler un bon endroit pour une variable 8 bits, mais une variable 32 bits peut produire moins de code.

Robert
la source
Cela peut sauver du code mais il va manger de la RAM.
mikeselectricstuff
Il ne consommera de RAM que si cet appel de fonction se trouve dans la branche la plus longue de la trace d'appel. Sinon, il réutilise l'espace de pile dont une autre fonction a déjà besoin.
Robert
2
Habituellement vrai, à condition qu'il s'agisse d'une variable locale. Si elle manque de RAM, la taille des vars globaux, en particulier les tableaux, est un bon endroit pour commencer à rechercher des économies.
mikeselectricstuff
1
Une autre possibilité, intéressante, est de remplacer les variables non signées par signé. Si un compilateur optimise un court-métrage non signé dans un registre 32 bits, il doit ajouter du code pour garantir que sa valeur passe de 65535 à zéro. Si, cependant, le compilateur optimise un court-métrage signé dans un registre, aucun code de ce type n'est requis. Parce qu'il n'y a aucune garantie de ce qui se passera si un court-circuit est incrémenté au-delà de 32767, les compilateurs ne sont pas tenus d'émettre du code pour y faire face. Sur au moins deux compilateurs ARM que j'ai examinés, le code court signé peut être plus petit que le code court non signé pour cette raison.
supercat