Capsule : Compilateur et interpréteur

Introduction

Les langages de programmation sont fréquemment classifiés en langages compilés d’un côté et langages interprétés de l’autre. Les premiers étant généralement considérés comme des langages puissants et performants alors que les seconds seraient des langages limités, et plutôt lents. Cette capsule a pour but de remettre en question cette classification et montrer qu’elle n’est pas pertinente. Pour cela, nous examinons d’abord ce qu’est un compilateur, puis ce qu’est un interpréteur et, avant de conclure, nous revenons sur les termes langages compilés et langages interprétés pour examiner leur pertinence.

Compilateur

Un compilateur est un programme capable de traduire un programme écrit dans un certain langage (code source) en un programme équivalent écrit dans un autre langage (code objet). Deux cas de figure sont possibles :

  • Le langage du code objet est un langage machine d’un processeur physique.
  • Le langage du code objet est un langage de programmation ou un bytecode

Dans le premier, on parle d’un compilateur natif. C’est le cas notamment du compilateur gcc capable de traduire les langages C, C++, Ojective-C, Ada, Fortran et Go vers le langage machine de différents processeurs. Toutefois, le code objet produit n’est pas encore exécutable. Pour qu’il le soit, une dernière étape est encore nécessaire : l’édition de liens (linking). Durant cette étape, le code objet est lié aux différentes bibliothèques (.lib) utilisées par le programme et l’ensemble est empaqueté dans un fichier dont le format spécifique au système d’exploitation : le format PE pour Windows (.exe et .dll), le format ELF pour la plupart des systèmes de la famille Unix, le format PEF pour Mac OS.

Dans le second cas, on parle de compilateur si le langage du code objet est un bytecode, et on parle de compilateur, de transcompilateur ou encore de transpileur (transpiler) si le code objet produit est un langage de programmation. Le plus souvent, même lorsque le langage du code objet est un langage de programmation. Le programme javac.exe est compilateur capable de traduire le Java en Java Bytecode. La transcompilation est fréquemment utilisée pour dans le développement d’application web. Le programme tsc est un (trans)compilateur capable de traduire le TypeScript en JavaScript.

Pour résumer, un compilateur est un traducteur.

Interpréteur

Pour exécuter un programme, on a besoin d’un calculateur universel (universal computing machine) capable d’exécuter les instructions dont il est composé. Si ces instructions sont destinées à un processeur physique, p. ex. un processeur de la famille x86 ou un processeur de la famille ARM, le calculateur universel est un ordinateur et son système d’exploitation. Dans le cas contraire, le calculateur universel est lui-même un programme que l’on appelle interpréteur. La plupart des interpréteurs sont capables d’exécuter les instructions d’un langage de programmation c’est-à-dire des instructions sous forme de texte. Par exemple, l’interpréteur php.exe est capable d’exécuter les instructions du langage PHP et l’interpréteur de python.exe est capable d’exécuter les instructions du langage Python. Les autres, plus rares, exécutent les instructions d’un langage machine, c’est-à-dire des instructions codées par des nombres. Les langages machine interprétés sont communément appelés « bytecodes » ou « langages intermédiaires ». Par exemple, l’interpréteur java.exe est capable d’exécuter les instructions du Java ByteCode. Le Java Bytecode étant un bytecode au même titre que le MSIL du Framework .NET.

Le plus souvent un interpréteur a le même nom que le langage. Par exemple, l’interpréteur du langage Python est le programme python, l’interpréteur du langage Bash est le programme bash et l’interpréteur du langage PowerShell est le programme PowerShell. JavaScript est une exception notable puisque l’interpréteur le plus courant est le programme node. Pour lancer l’exécution d’un programme avec l’interpréteur, la commande est généralement de la forme : <nom_interpréteur> <chemin_fichier_source>.

1
2
3
4
5
java mon_programme.class
python mon_programme.py
node mon_programme.js
bash mon_programme.sh
powershell .\mon_programme.ps1
Fig. 1 – Exécution de programmes à l’aide de différent interpréteur.

S’il est exécuté sans paramètre, un interpréteur démarre le plus souvent dans un mode interactif et affiche une invite de commande (command prompt). C’est évidemment toujours le cas pour les langages de commande. S’il n’a pas de mode interactif (php, perl, ruby, etc.), il lit généralement les caractères du clavier jusqu’à la saisie d’un caractère EOF (end of file) avec la combinaison de touches ctrl-D, avant d’exécuter le programme saisi.

1
2
3
4
5
6
7
8
9
10
$ python
Python 2.7.15 (default, Jan 12 2019, 21:07:57) 
[GCC 4.2.1 Compatible Apple LLVM 10.0.0 (clang-1000.11.45.5)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> print "hello world!"
hello world!
>>> a = 2 + 2
>>> print a
4
>>> 
Fig. 2 – Interpréteur de Python en mode interactif. Le symole >>> est l’invite de commande.

En bref, un interpréteur est un programme qui implémente un calculateur universel capable d’exécuter les instructions d’un langage, dont le nom est typiquement celui du langage pour lequel il est conçu et qui implémente ou non un mode interactif.

Langage compilé et langage interprété

Les deux sections précédentes ont mis en évidence qu’un compilateur et un interpréteur sont deux choses totalement distinctes et que l’usage d’une n’exclut pas l’usage de l’autre.

Il existe bel et bien des langages, traditionnellement utilisés pour la programmation de logiciel système, tel que C, C++ ou Ada, pour lesquel il n’existe pas d’interpréteur. Mais dans l’univers des langages de programmation, ceux-ci font plutôt figure d’exceptions. En effet, compilation et interprétation sont le plus souvent utilisées de paire.

Prenons, comme premier exemple, le langage Java. Un programme en Java doit d’abord être compilé (traduit) en Java Bytecode (un langage machine portable) à l’aide du compilateur javac et le code produit peut ensuite être exécuté par l’interpréteur java. Toutefois, dans la plupart des cas, l’interpréteur java utilise pour ce faire un compilateur JAT (just in time compiler ou JIT compiler) pour compiler le Bytecode dans le langage machine du processeur de la plateforme. La vitesse d’exécution est donc très proche de ce que l’on aurait avec un langage compilé nativement. Seul le temps de chargement est plus long. Les langages de la plateforme .NET mettent en œuvre un principe similaire.

Considérons, comme second exemple, le langage Python. Traditionnellement, pour exécuter un programme Python, on passe le fichier contenant ce programme à l’interpréteur python. Ce dernier commence par compiler le code dans un langage intermédiaire puis interprète le code produit, le tout de manière totalement transparente. Toutefois, il est également possible de compiler un programme Python en C à l’aide du compilateur Cython, puis d’utiliser un compilateur natif tel que gcc ou clang pour produire un exécutable. Cette stratégie, qui consiste à compiler le code source vers du C, est également en mise en œuvre pour le langage Eiffel.

Ces quelques exemples mettent en évidence le fait que l’utilisation d’un interpréteur est bien souvent combinée avec celle d’un ou plusieurs compilateurs et donc, qu’au sens strict, un langage interprété est bien souvent également un langage compilé.

Conclusion

Nous avons montré que la classification des langages de programmation en langages compilés d’un côté et langages compilés de l’autre n’est pas possible et n’a donc aucune pertinence. Même si un programme natif peut être plus rapide qu’un programme interprété, les problèmes de performance sont plus souvent imputables à l’utilisation d’algorithmes naïfs qu’a la manière d’exécuter le programme.