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

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.

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 .

Notes…

[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.