Présentation…
Ada est une merveille de langage. GNAT étant un compilateur Ada, et qui de plus nous fait même le cadeau de fonctionner sous Windows 95 et 98, on pourrais êtres tentés de l’applaudir inconditionnellement. Malheureusement, GNAT souffre d’un défaut qui pourra être rédhibitoire si la légèreté est un critère : la taille des exécutables produits par GNAT est nettement plus grande que la moyenne et même parfois énorme, et ce défaut bien connu n’a jamais été résolu par l’équipe de GNAT et qui a probablement d’autres priorités, comme la fiabilité des grandes applications et que l’on pourra tout de même saluer pour tout le reste de son travail.
Dans cette page …
- Contexte et présentation du problème
- Présentation de la solution
- Ne lier que ce qui est nécessaire
- Récupérer l’information sémantique
- Récupérer les informations de liaisons ( « binding » )
- Compiler sans code de test
- L’optimisation du code
- Tenir compte des librairies standards
- Re-compilation avec les directives
- Mise en œuvre
- Efficacité de la méthode
- Bilan
- Vos commentaires
- La suite…
- Notes
Contexte et présentation du problème
C’est un fait avéré que l’évolution du logiciel, depuis environ 10 ans, est sur une très mauvaise pente concernant la consommation de ressource. Plus aucun soin n’est apporté à la gestion des ressources, tant de mémoire que de disque, et les logiciels contemporains consomment de plus en plus de ressources, pour des fonctionnalités qui autrefois pourtant en auraient nécessité 10 fois moins. Les environnements dit « managés » ne font qu’aggraver la situation, et participent à répandre cette culture du gaspillage insouciant : il faut le reconnaître de plus en plus on dilapide les précieuses ressources informatiques, comme d’autres dilapideraient la forêt Amazonienne.
Les problèmes se posent, tant pour ceux et celles qui travaillent encore ( à titre personnel ou professionnel ) sur d’anciennes machines, que pour ceux et celles qui bien qu’ayant accès à des machines raisonnablement bien équipées, pensent malgré tout que les ressources sont faites pour êtres employées efficacement à la tâche, et non pas pour être gaspillées.
Dans ces conditions, un compilateur GNAT qui produit
des simples « Hello World » pesant
plus de 200 KB peut à raison rendre un peu fou. La solutions proposées
ici, va plus loin que celle qui est proposée sur le comp.lang.ada lui-même, et qui se limite à proposer un « strip
» [ note 1 ] sur l’exécutable.
Présentation de la solution
« Striper » l’exécutable est bien sûre une solution inévitable, mais insuffisante, car l’une des causes qui rend les exécutables produits par GNAT si trop gros, est la runtime définie par le langage Ada. C’est la cause la plus flagrante pour le petites applications, et on peut en limiter légèrement les effets ( ce qui est toujours ça de gagné ). Mais sur les applications de taille plus conséquente, vient une autre source d’alourdissement de la taille du binaire produit: avec GNAT, quand une unité fait référence à une autre unité, alors l’unité référencée sera liée en totalité, toute entière à la l’unité référente. Si vous développez un paquet ( package ) contenant beaucoup de méthodes pour un domaine donné, et que vous créez ensuite une application qui référence, même ne serait-ce qu’une seule de ces méthodes, alors tout le paquet sera lié. Le binaire contiendra alors disons par exemple les 150 méthodes de votre paquet, même s’il n’en utilisera qu’une seule : c’est un gâchis, dommage.
Ada est un descendant de Pascal. Et il existait justement un compilateur Pascal, qui m’a laissé de très bons souvenirs ( mes meilleurs souvenirs même ), qui était TurboPascal. Dans le fichier d’aide du TurboPascal, il était dit qu’il ne fallait pas hésité à remplir une unité avec autant de méthodes ( procédures et fonctions ) que nécessaire, parce que le lieur de TurboPascal ne liait que ce qui était effectivement utilisé.
C’est une solution semblable qui serait la mieux appropriées pour GNAT. Malheureusement, GNAT emploie le format de fichier objet standard. Et il est bien connu que ce format n’est pas économe, puisque lui aussi implique que la seul référence à une méthode contenu dans un fichier objet, implique de lier la totalité du fichier objet. TurboPascal quand à lui, était beaucoup plus astucieux, car il employait un format spécial, spécialement adapté à l’économie de liaison.
Bien que GNAT soit bien loin d’être aussi efficace que TurboPascal à cette tâche ( et que de plus le moyen d’y parvenir soit beaucoup plus long est fastidieux ), ne nous privons pas d’en obtenir ce que l’on peut en obtenir. En bon(ne)s développeur(se)s, sachons faire avec ce que l’on a… en espérant mieux un jour ( et surtout, n’abandonnons pas Ada pour ce défaut dont-il n’est pas responsable ).
Ne lier que ce qui est nécessaire
Nous allons passer en revu, différentes procédures permettant de produire un programme ne contenant pas ce qui n’est pas nécessaire à son exécution.
Récupérer l’information sémantique
Avec GNAT, il est possible de ne lier que ce qui
est nécessaire. Il faut tout d’abord savoir que GNAT
permet de produire un fichier issu de la compilation, et permettant
d’avoir une représentation de la structure de la sémantique d’un
programme complet, et de ses dépendances. Ce sont les fichiers d’arbre
sémantiques ( tree-files ),
que GNAT stocke, dans des fichiers « *.adt
». Ces fichiers ne sont produits par GNAT
que si on lui demande. On lui indiquera de le faire, par le biais
de l’option « -gnatt
». En employant
cette option, il faudra aussi passer à GNAT les options
« -gnatc
» et « -c
». La première demande à GNAT de ne produire
aucun code, et la deuxième indique à GCC qu’aucune
liaison ne devra être effectuée ( il est nécessaire d’indiquer
les deux, car la présence de la première ne suffit pas à induire
automatiquement la deuxième… ce qui devrait pourtant être le cas ).
Cette information sémantique devra être analysée pour distinguer
ce qui devra être conservé de ce qui devra être éliminé. C’est « gnatelim
», un utilitaire fourni avec
GNAT qui aura la charge de cette opération. La compilation
sera ensuite lancée en tenant compte des directives de nettoyages
produites par « gnatelim
».
De ce qui précède, on devinera bien que l’opération se fait en
trois passes. Il faudra tout d’abord effectuer une compilation à
blanc, pour créer l’information sémantique requise par « gnatelim
». Ensuite invoquer « gnatelim
»
pour qu’il détermine ce qui devra être retiré de la compilation
réelle. Car en effet, il ne s’agit pas d’exclure des éléments à
l’étape de la liaison ( ce qui est rendu impossible par le
format des fichiers objet ), mais bien de re-compiler, en
excluant certaines parties du code de la compilation, ce qui les
exclura donc des fichiers objets, et finalement de l’exécutable
produit par la liaison de ces binaires.
Pour que GNAT exclu des parties de code de la compilation,
il faut les lui indiquer par des « pragmas Eliminate
». Cette directive ( pragma )
ne sera pas décrite ici, car étant produite automatique par « gnatelim
», vous n’avez pas à les créer
manuellement. Vous aurez par contre éventuellement à en supprimer
quelques-unes : il arrive ( rarement ) que « gnatelim
» fasse erreur et qu’il produise une directive d’élimination
d’une partie de code qui est pourtant bien référencée par le programme
( encore une signe qu’il y a un peu de bricolage là-dessous,
et que GNAT est loin d’être au point à ce sujet… mais
ne considérons pas pour autant qu’il en est bon à jeter… loin de
là ).
Ces « pragmas Eliminate
» peuvent
être employées comme pragmas de configuration,
et pourraient donc être placées dans le fichier « gnat.adc
». Toutes fois, il sera plus aisé de les placer dans
un autre fichier de configuration. En effet, il serait peu commode
de nettoyer le fichier « gnat.adc
»
à chaque re-compilation, tandis qu’en plaçant ces pragmas dans un fichier propre, on se réserve « gnat.adc
» pour les pragmas de configuration
conçues par le ou la développeur(se). Il est effectivement possible
d’employer un second fichier de configuration avec GNAT
( un et un seul ), en lui indiquant ce fichier par l’option
de ligne de commande « -gnatec{path}
».
Nous nommerons ce fichier « elim.adc
».
Si votre processus de développement passait par un fichier de configuration
transmis par l’option « -gnatec
»,
il faudra peut-être réorganiser le processus à la manière qui vous
conviendra le mieux.
Pour quelques considérations sur les fichiers de configuration, vous pourrez vous référez à Abrégé du guide d’utilisation de GNAT .
Récupérer les informations de liaisons ( « binding » )
Une chose vient maintenant : « gnatelim
»
a besoin de plus que du seul fichier de représentation sémantique.
« Gnatelim
» a également besoin du
fichier de « bind
» produit par « gnatbind
». Pour produire ce fichier,
il faudra compiler le programme, sans liaison, mais sans faire une
compilation à blanc. « Gnatbind
» utilise
les fichiers « *.ali
»,
produits dans le même temps que la compilation des fichiers objets.
À priori, on pourrait penser qu’il serait plus économe de créer
le fichier sémantique en même temps que les fichiers *« .ali
» ( en fournissant l’option « -gnatt
» à ce moment là ) nécessaires à « gnatbind
». Malheureusement, il apparaît que GNAT
ne fonctionne pas correctement dans ces conditions, et qu’il se
produit des erreurs pendant une partie du processus qui vous est
présenté ici. Il nous faudra donc ajouter encore une passe supplémentaire.
Le processus nécessitera donc finalement quatre passes.
Compiler sans code de test
Les différents codes de tests de validité sont consommateurs d’espace
( et de temps d’exécution ). La génération de ces codes
pourra être désactivée en totalité avec l’option « -gnatp
».
Pour découvrir l’option « -gnatp
»
vous pourrez vous référez à Abrégé du guide d’utilisation de GNAT
.
L’optimisation du code
L’optimisation du code avec les options « -O1
», « -O2
» et « -O3
» participe également à la taille du code produit.
Pour quelques considérations sur les effets des options d’optimisation, vous pourrez vous référez à Abrégé du guide d’utilisation de GNAT .
Tenir compte des librairies standards
Ce chapitre est très court, mais il est cependant d’une importance
majeur, et doit être bien présent à votre esprit. Si on applique
simplement de cette manière les techniques présentées précédemment,
elles ne seront appliquées qu’aux paquets du programme dont on traite
la compilation. Seulement, les librairies standards devraient êtres
incluses également dans ce processus, afin d’exclure de la compilation,
les méthodes inutilisées qui sont présentes dans les paquets standards.
Il faut savoir savoir que les librairies standards sont toutes fois
déjà elles-mêmes compilées par GNAT sans codes de test.
Pour inclure les librairies standards dans le processus, on emploiera
l’option « -a
». Peut-être vous inquiétez-vous
que cette option ne provoque la re-compilation des versions partagées
de ces librairies. C’est effectivement une question qui devrait
vous interroger. Vous pouvez êtres rassuré(e)s, car cette option
produit une re-compilation des librairies standards en local.
Pour découvrir l’option « -a » vous pourrez vous référez à Abrégé du guide d’utilisation de GNAT .
Re-compilation avec les directives
Finalement, le programme devra être re-compilé avec le fichier
de configuration contenant les « pragmas Eliminate
».
Mise en œuvre
Reprenons maintenant chacune des étapes présenté, est créons une
mise en œuvre de celles-ci. Le parti pris sera fait ici de les mettre
en œuvre dans un fichier script Windows ( batch ). Nos ami(e)s Linuxien(ne)s pourront adapter
à leur convenance. Dans tout le processus, nous ferons usage de
l’option « -f
», qui force la prise
en compte des fichiers sans considération de leurs dates de modification,
afin d’assurer l’intégrité de l’ensemble. En effet, l’usage d’options
et de pragmas de configuration nécessite
de forcer les re-compilations, car les changements d’options et
de pragmas de configuration ne marquent
pas automatiquement les fichiers concernés comme périmés. Sans cela,
certaines mises à jours seraient manquées. Nous passerons aussi
par un paramètre « %1
», qui
est la méthode habituelle pour récupérer le premier paramètre passé
à l’invocation d’un fichier batch sous
DOS et Windows.
Les lignes commençant par « rem
»
marquent des commentaires.
Pour comprendre ce qui suit, il importe que vous ayez lu les chapitres précédents.
Première étape : la création de l’information sémantique ne nécessite
aucune option de compilation particulière ( comme les options
d’optimisation ), car elle se fait à blanc. Nous maintiendrons
pourtant l’option « -gnatp
», pour
bien marquer le fait que les éventuelles méthodes nécessaires aux
codes de test ne seront pas considérées comme référencées. Cette
compilation étant à blanc, nous trouverons les options « -c
» et « -gnatc
».
rem production de l’information semantique (fichiers *.adt) gnatmake -c -a -f -gnatc -gnatt -gnatp %1
Deuxième étape : la production du fichier de liaisons de l’application
( bind ), nécessite une compilation
sans liaison, mais une véritable compilation tout de même. Nous
trouvons donc l’option « -c
» mais
pas l’option « -gnatc
».
rem production du fichier bind pour la totalite du programme gnatmake -c -a -f -gnatp -O2 %1 gnatbind %1
Troisième étape : la production des « pragmas Eliminate
» passe par « gnatelim
».
Les pragmas seront écrites dans le fichier
« elim.adc
», que nous dédions à ce
seul usage.
rem determination de ce qui devra etre exclus de la compilation gnatelim -a %1 >elim.adc
Quatrième et dernière étape : nous re-compilons finalement tout
le programme, en tenant compte des « pragmas Eliminate
». Le fichier « elim.adc
»
est transmis par l’option « -gnatecelim.adc
».
rem compilation de la version allegee gnatmake -a -f -gnatecelim.adc -gnatp -O2 %1 -largs -s
Le fichier de script Windows contenant ces instructions
pourra être nommé « release.bat
»,
et pourra être agrémenté pour l’améliorer de manière pratique, notamment
avec un test s’assurant qu’un paramètre désignant le programme à
compiler est bien passé à l’invocation du script. La version finale
pourrait donc être la suivante ( adaptable selon votre convenance )…
@echo off cls if not "%1"=="" goto debut echo. echo Erreur : vous devez donner le nom du programme a compiler echo Usage : release Nom-du-programme echo. goto fin :debut rem Un peu de menage, pour avoir un plan de travail propre. if exist *.o del *.o if exist *.ali del *.ali if exist *.adt del *.adt if exist elim.adc del elim.adc if exist %1.exe del %1.exe rem Le travail lui-meme. rem Production de l’information semantique (fichiers *.adt). gnatmake -c -a -f -gnatc -gnatt -gnatp %1 rem Production du fichier bind pour la totalite du programme. gnatmake -c -a -f -gnatp -O2 %1 gnatbind %1 rem Determination de ce qui devra etre exclus de la compilation. gnatelim -a %1 >elim.adc rem Compilation de la version allegee. gnatmake -a -f -gnatecelim.adc -gnatp -O2 %1 -largs -s rem Suppression des fichiers de compilation. rem Ils sont maintenant inutiles, rem puisque l’on cre une version release. del *.o del *.ali del *.adt del elim.adc :fin
Efficacité de la méthode
Le méthode a été testé sur un simple petit programme « HelloWorld
». Le binaire produit pèse environ 62 KB. Ce résultat
peut sembler décevant. Un autre test a été effectué, avec un tout
petit programme qui faisait référence à un gros package factice créé pour l’occasion. Ce gros package contenait 1024 fonctions simples. Le
programme de teste référençait ce package,
et n’invoquait qu’un seule des fonctions. Le binaire produit dans
ce cas pèse seulement 1 KB de plus que le programme de type « HelloWorld
». Il est clairement apparu
qu’il existe une part incompressible dans tous les binaires produit
par GNAT. Il ne s’agit donc pas d’un facteur multipliant
le poid des binaires GNAT par rapport au poids des
binaires de tout autre compilateur. Il s’agit d’un sur-poids initial
et constant. Ceci est une bonne nouvelle dans certains sens, car
plus le programme est important, et plus en proportion ce sur-poids
initial constant sera faible par rapport au poids de tout le binaire
de l’application. Malheureusement, ce sur-poids original est de
tout de même plus de 62 KB, c’est à dire presque 64 KB, ce qui était
par exemple la taille d’un segment mémoire entier sur les anciens
ordinateurs.
Bilan
GNAT ne semble pas être adapté à la création de petit programme. Ce qui est bien dommage, car conceptuellement, il y a pourtant un véritable intérêt à créer même les petits en Ada. Si de tels programme sont destinés à la distribution, il faudra de préférence passer par un autre langage, comme Pascal, en souhaitant que les compilateurs Pascal disponibles n’ont pas le même défaut. Si ces applications sont destinés à un usage personnel sur des postes n’étant pas trop limités en espace, il n’y a pas de contre-indications à rester fidèle à Ada même pour les petites applications. Pour les applications destiné à être utilisé comme CGI sur des serveurs ( les CGI sont lancés et terminés à chaque requêtes, … leur initialisation doit donc être très rapide ), il serait nécessaire d’obtenir des mesures de l’impact de se sur-poids sur le chargement des binaires à chaque invocations.
Vos commentaires
Vos commentaires sur cette question cruciale de la réduction de la taille des exécutables produit par GNAT, ainsi que sur la solution proposée ici ( et même sa présentation ), sont les bien venus. La page de commentaire du site est à votre disposition : page d’envoi de commentaire ; ou selon votre préférence, l’e-mail du site : les-ziboux@rasama.org .
La suite…
Compiler pour une autre cible que Windows : Configurer une machine dédiée à la compilation Ada GNAT ciblant Linux
Notes…
Retour en haut de page Retour à la table de la page
[Note 1] -
Strip est un mot anglais signifiant « retirer,
dénuer, dépouiller, déshabiller, etc ». L’opération de « stripage »
sur un binaire exécutable ou sur un binaire objet, est une opération
qui consiste en la suppression de symboles contenus dans le binaire,
et qui sont inutiles à son fonctionnement normal. Ces symboles sont
le plus souvent des symboles de débogage, et dès lors que l’on crée
un binaire destiné à la distribution, ces informations de débogage pourront
être omises. Cette opération peut être effectuées soit par le lieur,
avec par exemple l’option « -s
» de « ld
» ( le lieur de GCC )
ou soit par un programme spécial nommé « strip
»
qui prend le nom du binaire en argument.
Retour : Contexte et présentation du problème