Qu'est-ce que LLVM? La puissance derrière Swift, Rust, Clang et plus

De nouvelles langues et des améliorations par rapport aux langues existantes se multiplient dans le paysage du développement. Mozilla's Rust, Apple's Swift, Jetbrains's Kotlin et de nombreux autres langages offrent aux développeurs une nouvelle gamme de choix pour la vitesse, la sécurité, la commodité, la portabilité et la puissance.

Pourquoi maintenant? Une des principales raisons réside dans les nouveaux outils de création de langages, en particulier les compilateurs. Et le principal d'entre eux est LLVM, un projet open source développé à l'origine par le créateur du langage Swift Chris Lattner dans le cadre d'un projet de recherche à l'Université de l'Illinois.

LLVM facilite non seulement la création de nouveaux langages, mais également le développement de langages existants. Il fournit des outils pour automatiser la plupart des parties les plus ingrates de la tâche de création de langage: créer un compilateur, porter le code produit sur plusieurs plates-formes et architectures, générer des optimisations spécifiques à l'architecture telles que la vectorisation et écrire du code pour gérer les métaphores du langage commun comme exceptions. Grâce à ses licences libérales, il peut être librement réutilisé en tant que composant logiciel ou déployé en tant que service.

La liste des langues utilisant LLVM a de nombreux noms familiers. Le langage Swift d'Apple utilise LLVM comme cadre de compilation, et Rust utilise LLVM comme composant principal de sa chaîne d'outils. De plus, de nombreux compilateurs ont une édition LLVM, comme Clang, le compilateur C / C ++ (c'est le nom, «C-lang»), lui-même un projet étroitement lié à LLVM. Mono, l'implémentation .NET, a une option pour compiler en code natif à l'aide d'un back-end LLVM. Et Kotlin, en principe un langage JVM, développe une version du langage appelé Kotlin Native qui utilise LLVM pour compiler en code natif machine.

LLVM défini

En son cœur, LLVM est une bibliothèque permettant de créer par programmation du code natif machine. Un développeur utilise l'API pour générer des instructions dans un format appelé représentation intermédiaire ou IR. LLVM peut ensuite compiler l'IR dans un binaire autonome ou effectuer une compilation JIT (juste à temps) sur le code à exécuter dans le contexte d'un autre programme, tel qu'un interpréteur ou un runtime pour le langage.

Les API de LLVM fournissent des primitives pour développer de nombreuses structures et modèles communs trouvés dans les langages de programmation. Par exemple, presque tous les langages ont le concept d'une fonction et d'une variable globale, et beaucoup ont des coroutines et des interfaces de fonction étrangère C. LLVM a des fonctions et des variables globales comme éléments standard dans son IR, et a des métaphores pour créer des coroutines et s'interfacer avec les bibliothèques C.

Au lieu de passer du temps et de l'énergie à réinventer ces roues particulières, vous pouvez simplement utiliser les implémentations de LLVM et vous concentrer sur les parties de votre langage qui nécessitent une attention particulière.

En savoir plus sur Go, Kotlin, Python et Rust 

Aller:

  • Appuyez sur la puissance de la langue Go de Google
  • Les meilleurs IDE et éditeurs de langage Go

Kotlin:

  • Qu'est-ce que Kotlin? L'alternative Java expliquée
  • Framework Kotlin: un aperçu des outils de développement JVM

Python:

  • Qu'est-ce que Python? tout ce que tu as besoin de savoir
  • Tutoriel: Comment démarrer avec Python
  • 6 bibliothèques essentielles pour chaque développeur Python

Rouille:

  • Qu'est-ce que la rouille? Le moyen de faire du développement logiciel sûr, rapide et facile
  • Apprenez à démarrer avec Rust 

LLVM: conçu pour la portabilité

Pour comprendre LLVM, il peut être utile de considérer une analogie avec le langage de programmation C: C est parfois décrit comme un langage d'assemblage portable et de haut niveau, car il a des constructions qui peuvent être étroitement liées au matériel système, et il a été porté sur presque chaque architecture système. Mais C n'est utile en tant que langage d'assemblage portable que jusqu'à un certain point; il n'a pas été conçu dans ce but précis.

En revanche, l'IR de LLVM a été conçu dès le début pour être un assemblage portable. Une façon de réaliser cette portabilité consiste à offrir des primitives indépendantes de toute architecture de machine particulière. Par exemple, les types entiers ne sont pas limités à la largeur de bits maximale du matériel sous-jacent (comme 32 ou 64 bits). Vous pouvez créer des types d'entiers primitifs en utilisant autant de bits que nécessaire, comme un entier de 128 bits. Vous n'avez pas non plus à vous soucier de la fabrication de la sortie pour qu'elle corresponde au jeu d'instructions d'un processeur spécifique; LLVM s'en charge également pour vous.

La conception neutre en architecture de LLVM facilite la prise en charge du matériel de toutes sortes, présent et futur. Par exemple, IBM a récemment contribué au code pour prendre en charge ses architectures z / OS, Linux on Power (y compris la prise en charge de la bibliothèque de vectorisation MASS d'IBM) et AIX pour les projets C, C ++ et Fortran de LLVM. 

Si vous souhaitez voir des exemples en direct de LLVM IR, accédez au site Web du projet ELLCC et essayez la démo en direct qui convertit le code C en LLVM IR directement dans le navigateur.

Comment les langages de programmation utilisent LLVM

Le cas d'utilisation le plus courant de LLVM est celui d'un compilateur AOT pour un langage. Par exemple, le projet Clang compile à l'avance C et C ++ en binaires natifs. Mais LLVM permet également d'autres choses.

Compilation juste à temps avec LLVM

Certaines situations nécessitent que le code soit généré à la volée au moment de l'exécution, plutôt que compilé à l'avance. Le langage Julia, par exemple, compile son code en JIT, car il doit s'exécuter rapidement et interagir avec l'utilisateur via une REPL (boucle de lecture-évaluation-impression) ou une invite interactive. 

Numba, un package d'accélération mathématique pour Python, compile en JIT certaines fonctions Python en code machine. Il peut également compiler du code décoré par Numba à l'avance, mais (comme Julia) Python offre un développement rapide en étant un langage interprété. L'utilisation de la compilation JIT pour produire un tel code complète le flux de travail interactif de Python mieux que la compilation anticipée.

D'autres expérimentent de nouvelles façons d'utiliser LLVM en tant que JIT, comme la compilation de requêtes PostgreSQL, ce qui permet de multiplier par cinq les performances.

Optimisation automatique du code avec LLVM

LLVM ne compile pas seulement l'IR en code machine natif. Vous pouvez également le diriger par programme pour optimiser le code avec un degré élevé de granularité, tout au long du processus de liaison. Les optimisations peuvent être assez agressives, y compris des choses comme l'intégration de fonctions, l'élimination du code mort (y compris les déclarations de type et les arguments de fonction inutilisés) et le déroulement des boucles.

Encore une fois, le pouvoir est de ne pas avoir à mettre en œuvre tout cela vous-même. LLVM peut les gérer pour vous, ou vous pouvez lui demander de les désactiver selon vos besoins. Par exemple, si vous voulez des binaires plus petits au prix de quelques performances, vous pouvez demander à l'interface du compilateur de dire à LLVM de désactiver le déroulement de boucle.

Langages spécifiques au domaine avec LLVM

LLVM a été utilisé pour produire des compilateurs pour de nombreux langages à usage général, mais il est également utile pour produire des langages hautement verticaux ou exclusifs à un domaine de problème. À certains égards, c'est là que LLVM brille le plus, car il supprime une grande partie de la corvée dans la création d'un tel langage et le rend performant.

Le projet Emscripten, par exemple, prend le code LLVM IR et le convertit en JavaScript, permettant en théorie à n'importe quel langage avec un back-end LLVM d'exporter du code qui peut s'exécuter dans le navigateur. Le plan à long terme est d'avoir des back-ends basés sur LLVM qui peuvent produire WebAssembly, mais Emscripten est un bon exemple de la flexibilité de LLVM.

Une autre façon d'utiliser LLVM consiste à ajouter des extensions spécifiques au domaine à un langage existant. Nvidia a utilisé LLVM pour créer le compilateur Nvidia CUDA, qui permet aux langages d'ajouter un support natif pour CUDA qui se compile dans le cadre du code natif que vous générez (plus rapide), au lieu d'être appelé via une bibliothèque livrée avec (plus lent).

Le succès de LLVM avec les langages spécifiques au domaine a stimulé de nouveaux projets au sein de LLVM pour résoudre les problèmes qu'ils créent. Le plus gros problème est de savoir comment certains DSL sont difficiles à traduire en LLVM IR sans beaucoup de travail acharné sur le front-end. Une solution en cours est la représentation intermédiaire à plusieurs niveaux, ou projet MLIR.

MLIR fournit des moyens pratiques pour représenter des structures de données et des opérations complexes, qui peuvent ensuite être traduites automatiquement en LLVM IR. Par exemple, le cadre d'apprentissage automatique TensorFlow pourrait avoir un grand nombre de ses opérations complexes de graphe de flux de données compilées efficacement en code natif avec MLIR.

Travailler avec LLVM dans différentes langues

La manière typique de travailler avec LLVM consiste à utiliser du code dans un langage avec lequel vous êtes à l'aise (et qui prend en charge les bibliothèques de LLVM, bien sûr).

Deux choix de langage courants sont C et C ++. De nombreux développeurs LLVM utilisent par défaut l'un de ces deux pour plusieurs bonnes raisons: 

  • LLVM lui-même est écrit en C ++.
  • Les API de LLVM sont disponibles dans les incarnations C et C ++.
  • Une grande partie du développement du langage a tendance à se produire avec C / C ++ comme base

Pourtant, ces deux langues ne sont pas les seuls choix. De nombreux langages peuvent appeler nativement des bibliothèques C, il est donc théoriquement possible d'effectuer le développement LLVM avec un tel langage. Mais il est utile d'avoir une véritable bibliothèque dans le langage qui enveloppe élégamment les API de LLVM. Heureusement, de nombreux langages et environnements d'exécution de langage ont de telles bibliothèques, notamment C # /. NET / Mono, Rust, Haskell, OCAML, Node.js, Go et Python.

Une mise en garde est que certaines des liaisons de langage à LLVM peuvent être moins complètes que d'autres. Avec Python, par exemple, il existe de nombreux choix, mais chacun varie dans sa complétude et son utilité:

  • llvmlite, développé par l'équipe qui crée Numba, est devenu le candidat actuel pour travailler avec LLVM en Python. Il implémente uniquement un sous-ensemble des fonctionnalités de LLVM, tel que dicté par les besoins du projet Numba. Mais ce sous-ensemble fournit la grande majorité de ce dont les utilisateurs LLVM ont besoin. (llvmlite est généralement le meilleur choix pour travailler avec LLVM en Python.)
  • Le projet LLVM maintient son propre ensemble de liaisons à l'API C de LLVM, mais elles ne sont actuellement pas maintenues.
  • llvmpy, la première liaison Python populaire pour LLVM, est tombée en panne de maintenance en 2015. Mauvais pour tout projet logiciel, mais pire lorsque vous travaillez avec LLVM, étant donné le nombre de changements qui interviennent dans chaque édition de LLVM.
  • llvmcpy vise à mettre à jour les liaisons Python pour la bibliothèque C, à les maintenir à jour de manière automatisée et à les rendre accessibles en utilisant les idiomes natifs de Python. llvmcpy en est encore à ses débuts, mais peut déjà faire un travail rudimentaire avec les API LLVM.

Si vous êtes curieux de savoir comment utiliser les bibliothèques LLVM pour créer un langage, les propres créateurs de LLVM ont un tutoriel, utilisant C ++ ou OCAML, qui vous guide dans la création d'un langage simple appelé Kaleidoscope. Il a depuis été porté dans d'autres langues:

  • Haskell:  un port direct du didacticiel original.
  • Python: l' un de ces ports suit de près le didacticiel, tandis que l'autre est une réécriture plus ambitieuse avec une ligne de commande interactive. Les deux utilisent llvmlite comme liaisons avec LLVM.
  • Rust  and  Swift: Il semblait inévitable que nous obtenions des ports du didacticiel dans deux des langages que LLVM a contribué à faire exister.

Enfin, le tutoriel est également disponible en  langues humaines . Il a été traduit en chinois, en utilisant les originaux C ++ et Python.

Ce que LLVM ne fait pas

Avec tout ce que LLVM fournit, il est également utile de savoir ce qu'il ne fait pas.

Par exemple, LLVM n'analyse pas la grammaire d'une langue. De nombreux outils font déjà ce travail, comme lex / yacc, flex / bison, Lark et ANTLR. L'analyse syntaxique est censée être découplée de la compilation de toute façon, il n'est donc pas surprenant que LLVM n'essaie pas de résoudre tout cela.

LLVM ne traite pas non plus directement de la culture plus large des logiciels autour d'une langue donnée. Installer les binaires du compilateur, gérer les packages dans une installation et mettre à niveau la chaîne d'outils - vous devez le faire vous-même.

Enfin, et le plus important, il existe encore des parties communes des langages pour lesquelles LLVM ne fournit pas de primitives. De nombreux langages ont une certaine manière de gérer la mémoire récupérée, soit comme moyen principal de gérer la mémoire, soit comme complément à des stratégies comme RAII (que C ++ et Rust utilisent). LLVM ne vous donne pas de mécanisme de ramasse-miettes, mais il fournit des outils pour implémenter le ramasse-miettes en permettant au code d'être marqué avec des métadonnées qui facilitent l'écriture des garbage collector.

Cependant, rien de tout cela n'exclut la possibilité que LLVM puisse éventuellement ajouter des mécanismes natifs pour implémenter le garbage collection. LLVM se développe rapidement, avec une version majeure tous les six mois environ. Et le rythme de développement ne va probablement s'accélérer que grâce à la manière dont de nombreux langages actuels ont placé LLVM au cœur de leur processus de développement.