Cet article a une suite : « Avantage sémantique » des langages compilés (ou compilables).
Le langage de programmation Scheme, utilisé pour les enseignements dont il est question dans cette rubrique, est un langage interprété, mais il peut aussi être compilé. Cette dualité des modes de mise en œuvre est un avantage dont il faut profiter.
Interprétation vs. compilation
Un interprète est un programme qui simule une machine virtuelle dont le langage machine serait le langage de programmation utilisé (ici Scheme). Nous lui soumettons pour qu’il les « lise » des expressions rédigées en Scheme, si elles sont bien formées l’interprète les évalue (c’est-à-dire demande au processeur d’en calculer la valeur), affiche le résultat, puis est prêt à interpréter une autre expression du langage. Si notre expression est mal formée, l’interprète nous signale que nous avons commis une erreur de syntaxe. On appelle ce processus la boucle d’interrogation ou boucle d’interaction (read—eval—print loop, ou REP loop). On sait immédiatement si notre expression est syntaxiquement correcte, et si oui, on reçoit le résultat du calcul, sa valeur. C’est très agréable pour apprendre à programmer, on acquiert vite les bases syntaxiques et sémantiques du langage. C’est aussi très commode en phase de développement, les tests sont rapides.
Notons que l’existence (et l’usage) d’un interprète pour un langage de programmation donné suppose que le langage s’y prête, ce qui n’est pas le cas de tous ; cela dépend de la syntaxe et de la sémantique du langage. Pour fonctionner un interprète doit disposer des règles qui associent une valeur à chaque expression du langage. Ce n’est pas toujours possible.
Le processus de compilation d’un programme est différent : on écrit d’abord tout le programme, puis on le soumet à un logiciel appelé compilateur, dont le rôle est de « lire » le texte du programme (le programme source), puis de le traduire dans un autre langage, par exemple le langage machine de l’ordinateur sur lequel nous voulons l’exécuter. Alors que l’interprète nous propose de lui soumettre de façon interactive des expressions Scheme dont il nous rendra la valeur, comme un oracle, l’idée de la compilation consiste à prendre le texte Scheme d’un programme complet pour le traduire, en une fois, en langage machine directement exécutable, c’est-à-dire que le résultat de la compilation est un fichier exécutable que l’on pourra « lancer » comme une commande Unix.
Nous avions noté ci-dessus que tous les langages n’étaient pas susceptibles d’être interprétés : de même, certains langages ne peuvent être compilés. Le cas le plus typique est celui de Perl : pour compiler un programme, il faut disposer de son texte intégral en langage source, par définition pourrait-on dire, or si tout dans le langage est dynamique, c’est-à-dire sujet à modification au cours de l’exécution, de façon non prévisible, la compilation achoppe sur l’ignorance du texte à compiler. C’est aussi pourquoi les langages de la famille Lisp ont pendant longtemps été rétifs à la compilation, et qu’ils ne s’y sont finalement prêtés qu’au prix de certaines restrictions.
Pourquoi compiler ses programmes ?
Le processus de compilation est de prime abord moins agréable que l’interprétation : il faut commencer par écrire un texte assez long, et ne découvrir qu’après coup les erreurs de syntaxe que l’interprète nous aurait signalées immédiatement.
Cet inconvénient de la compilation, s’il est indubitable pour un programme ou un sous-programme très court, se transforme en avantage dès que l’on veut écrire un programme assez long et assez complexe, « assez long » pouvant commencer ici à une centaine de lignes et cinq ou six procédures. Voici trois raisons de compiler des programmes :
– le rapport de performance entre un code interprété et un code compilé avec un vrai compilateur est couramment entre un et deux ordres de grandeur, même avec des ordinateurs rapides cela mérite attention ;
– un programme compilé avec les librairies en statique peut être déployé sur des ordinateurs cibles qui ne possèdent ni l’environnement de développement ni l’environnement d’exécution ; pour une compilation avec librairies dynamiques, l’environnement d’exécution suffit ;
– dans un contexte pédagogique, la compilation est précieuse pour que les étudiants appréhendent le programme comme un objet informatique autonome ; dans l’environnement d’un interprète, il finit par s’instaurer une confusion entre « l’intérieur » et « l’extérieur » du programme, et les abstractions nécessaires sont difficiles à acquérir.
Ainsi, lorsqu’un débutant travaille depuis une heure ou deux dans le giron d’un interprète Scheme, par exemple, il arrive, d’expérience, qu’il ait du mal à distinguer les noms qui sont liés à des valeurs à cause d’une définition globale effectuée une heure avant, de ceux qui sont liés à des valeurs par une liaison locale à la procédure qu’ils sont en train d’écrire. Et la distinction local-global a un sens et une importance.
Pour prendre un exemple avec un autre langage, lorsque l’on installe un programme Perl auquel il manque un paquetage, il vous propose gentiment d’aller le chercher sur le site CPAN : c’est pratique sur le moment, mais lorsque, deux mois plus tard, on s’interroge sur l’environnement nécessaire à ce programme, on ne sait plus très bien.
Juste deux exemples en faveur de l’apprentissage de la notion de hiérarchisation des données, qui montrent la confusion qui peut résulter d’un contexte interprété (pas pour le vrai grand programmeur qui sait ce qu’il fait, bien sûr, mais pour le simple mortel). J’aurais pu ajouter les entrées-sorties.
Pour ce simple mortel, l’expérience de la compilation de son programme peut aider la compréhension. Me semble-t-il.
Pourquoi les programmes compilés sont-ils plus efficaces ?
La traduction d’un langage source en langage machine est une opération complexe. La compilation l’effectue une fois pour toutes, l’exécutable est en langage machine. L’interprète traduit chaque expression à chacune de ses exécutions.
Le compilateur dispose, pour traduire, de l’ensemble du texte du programme source, ce qui permet des optimisations. Si une même expression figure à plusieurs emplacements du programme, elle pourra n’être calculée qu’une fois. Le compilateur peut aussi détecter les « branches mortes » du programme, c’est-à-dire des expressions dont le calcul ne sert à rien parce que les résultats n’en sont jamais utilisés, et les supprimer. Et bien d’autres optimisations.
Qu’est-ce qu’un vrai compilateur Scheme ?
J’ai mentionné ci-dessus le gain de performance apporté par un vrai compilateur Scheme : en effet, beaucoup d’environnements de programmation Scheme ne proposent pas de vrai compilateur, mais peuvent construire des programmes exécutables qui sont en fait le programme désiré « emballé » avec l’interprète. L’exécution de ce programme se déroule en simulant une session interactive, où un robot qui tiendrait la place du programmeur soumettrait les expressions à l’interprète les unes après les autres. Avec une telle solution, la « compilation » (s’il est encore possible d’employer ce terme) apporte certes la commodité de fabriquer des exécutables, mais n’apporte pas le gain de performance attendu d’un compilateur. En outre de tels exécutables sont encombrants.
Pourquoi des auteurs respectables ont-ils eu recours à ce qui apparaît comme un subterfuge ? Ce n’est pas par négligence, il y des raisons à cela : les langages Lisp, et Scheme en est un, sont entièrement programmables dynamiquement, c’est-à-dire qu’un programme peut lire un texte Scheme au clavier, puis se l’incorporer pour l’exécuter comme s’il faisait partie de son texte initial. Compiler un programme qui se comporte ainsi impose peu ou prou d’incorporer un interprète à l’exécutable. Cela s’appelle eval.
Un vrai compilateur, par contre, traduira effectivement le texte du programme source en langage machine, en réalisant les optimisations et les « mises en facteur » que cette opération permet, mais au prix de certaines restrictions sur les aspects dynamiques du langage. Ces restrictions n’ont guère d’inconvénients pour un programme « réel », mais elles choquent les lispiens puristes.
La vraie compilation permet d’autres choses très utiles, comme la communication efficace avec des parties de programmes écrites dans d’autres langages. Ainsi le compilateur que j’utilise de préférence, Bigloo, permet de combiner des sous-programmes Scheme avec du C ou du Java. Dans le second cas le texte Scheme est traduit en code-octet (byte-code), ce qui permet son exécution par une machine virtuelle Java et son usage comme code mobile dans un navigateur Web.
Je ne connais qu’un véritable compilateur Scheme utilisable pour un travail réel : Bigloo.