Dernière modificiation le 20/11/2006
Le site des Hiboux

Réduire la taille des exécutables produits par GNAT

rechercher sur le site accueil mini FAQ Ada les paradigmes de programmation
 

Ada est une merveille de langage. GNAT étant un compilateur Ada, et qui de plus nous fait même le cadeau de fonctionner sous Win9X, on pourrais êtres tentés de l'applaudir inconditionnellement. Malheureusement, GNAT souffre d'un défaut qui pourra être rédhibitoire dans certaines circonstances : la taille des exécutables produits par GNAT est démesurément énorme, et ce défaut bien connu n'a jamais été résolu par l'équipe de GNAT, que l'on pourra tout de même saluer pour le reste de son travail.

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'agraver 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/celles qui travaillent encore (à titre personnel ou professionnel) sur d'anciennes machines, que pour ceux/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 200Ko 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 10] sur l'exécutable.

Présentation de la solution

Striper l'exécutable est bien sûre une solution inévitable, mais insuffisante. L'une des causes qui rend les exécutables produits par GNAT si énorme, 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 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 package sera lié. Le binaire contiendra alors disont par exemple les 150 méthodes de votre package, même s'il n'en utilisera qu'une seule. C'est très fort dommageable.

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écuperer 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 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 recompiler, 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 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 recompilation, 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/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écuperer les informations de 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 desactivé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 packages 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 packages 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 inquietez-vous que cette option ne provoque la recompilation 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 recompilation 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.

Recompilation avec les directives

Finalement, le programme devra être recompilé avec le fichier de configuration contenant les pragmas Eliminate.

Mise en oeuvre

Reprenons maintenant chacune des étapes présenté, est créons une mise en oeuvre de celles-ci. Le parti pris sera fait ici de les mettre en oeuvre dans un fichier batch Windows. Nos ami(e)s Linuxien(e)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 recompilations, 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/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 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 recompilons 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 batch 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 batch. 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 62KB. 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 1KB de plus que le programme de type « HelloWorld ». Il est clairement apparu qu'il existe une part incompréssible 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 poid des binaires de tout autre compilateur. Il s'agit d'un surpoid initial et constant. Ceci est une bonne nouvelle, car plus le programme est important, et plus en porportion ce surpoid initial constant est faible en proportion. Malheureusement, ce surpoid original est de tout de même plus de 62KB, c'est à dire presque 64KB, ce qui était par exemple la taille d'un segment mémoire entier sur les anciens ordinnateurs.

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 disponnibles n'ont pas le même défaut. Si ces applications sont destinés à un usage personnel sur des posts n'étant pas trop limité en espaces, 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, il serait nécéssaire d'obtenir des mesures de l'impact de se surpoid 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.

Google
 
Index

Gnat, un compilateur Ada

Suite de cette page

Configurer une machine dédiée à la compilation Ada GNAT ciblant Linux

Page dont celle-ci est la suite

Abrégé du guide d'utilisation de Gnat

Accueil du site

Accueil

Contact

Contact

Lien inactif

Info

Retour à la table de la page

[Note 10] - 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é 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.