Was ist LLVM? Die Kraft hinter Swift, Rust, Clang und mehr

In der gesamten Entwicklungslandschaft gibt es neue Sprachen und Verbesserungen gegenüber bestehenden. Mozillas Rust, Apples Swift, Jetbrains Kotlin und viele andere Sprachen bieten Entwicklern eine neue Auswahl an Optionen für Geschwindigkeit, Sicherheit, Komfort, Portabilität und Leistung.

Warum jetzt? Ein wichtiger Grund sind neue Tools zum Erstellen von Sprachen, insbesondere Compiler. Und der Chef unter ihnen ist LLVM, ein Open-Source-Projekt, das ursprünglich vom Swift-Sprachschöpfer Chris Lattner als Forschungsprojekt an der University of Illinois entwickelt wurde.

LLVM erleichtert es, nicht nur neue Sprachen zu erstellen, sondern auch die Entwicklung bestehender Sprachen zu verbessern. Es bietet Tools zur Automatisierung vieler der undankbarsten Teile der Aufgabe der Spracherstellung: Erstellen eines Compilers, Portieren des ausgegebenen Codes auf mehrere Plattformen und Architekturen, Generieren architekturspezifischer Optimierungen wie Vektorisierung und Schreiben von Code zur Verarbeitung gängiger Sprachmetaphern wie Ausnahmen. Aufgrund seiner liberalen Lizenzierung kann es frei als Softwarekomponente wiederverwendet oder als Dienst bereitgestellt werden.

Die Liste der Sprachen, die LLVM verwenden, hat viele bekannte Namen. Apples Swift-Sprache verwendet LLVM als Compiler-Framework und Rust verwendet LLVM als Kernkomponente seiner Toolkette. Viele Compiler haben auch eine LLVM-Edition, wie Clang, den C / C ++ - Compiler (dies ist der Name „C-lang“), selbst ein Projekt, das eng mit LLVM verbunden ist. Mono, die .NET-Implementierung, bietet die Option, mithilfe eines LLVM-Backends zu nativem Code zu kompilieren. Und Kotlin, nominell eine JVM-Sprache, entwickelt eine Version der Sprache namens Kotlin Native, die LLVM zum Kompilieren in maschinennativen Code verwendet.

LLVM definiert

LLVM ist im Kern eine Bibliothek zum programmgesteuerten Erstellen von maschineneigenem Code. Ein Entwickler verwendet die API, um Anweisungen in einem Format zu generieren, das als Zwischendarstellung oder IR bezeichnet wird. LLVM kann dann die IR in eine eigenständige Binärdatei kompilieren oder eine JIT-Kompilierung (Just-in-Time) für den Code durchführen, der im Kontext eines anderen Programms ausgeführt werden soll, z. B. eines Interpreters oder einer Laufzeit für die Sprache.

Die APIs von LLVM bieten Grundelemente für die Entwicklung vieler gängiger Strukturen und Muster in Programmiersprachen. Zum Beispiel hat fast jede Sprache das Konzept einer Funktion und einer globalen Variablen, und viele haben Coroutinen und C-Schnittstellen für Fremdfunktionen. LLVM hat Funktionen und globale Variablen als Standardelemente in seiner IR und Metaphern zum Erstellen von Coroutinen und zur Anbindung an C-Bibliotheken.

Anstatt Zeit und Energie zu investieren, um diese speziellen Räder neu zu erfinden, können Sie einfach die Implementierungen von LLVM verwenden und sich auf die Teile Ihrer Sprache konzentrieren, die die Aufmerksamkeit benötigen.

Lesen Sie mehr über Go, Kotlin, Python und Rust 

Gehen:

  • Tippen Sie auf die Go-Sprache von Google
  • Die besten Go-Sprach-IDEs und -Editoren

Kotlin:

  • Was ist Kotlin? Die Java-Alternative erklärt
  • Kotlin-Frameworks: Eine Übersicht über JVM-Entwicklungstools

Python:

  • Was ist Python? Alles, was Sie wissen müssen
  • Tutorial: Erste Schritte mit Python
  • 6 wichtige Bibliotheken für jeden Python-Entwickler

Rost:

  • Was ist Rost? Der Weg zur sicheren, schnellen und einfachen Softwareentwicklung
  • Erfahren Sie, wie Sie mit Rust beginnen 

LLVM: Entwickelt für Portabilität

Um LLVM zu verstehen, kann es hilfreich sein, eine Analogie zur Programmiersprache C in Betracht zu ziehen: C wird manchmal als tragbare Assemblersprache auf hoher Ebene beschrieben, da es Konstruktionen enthält, die eng mit der Systemhardware verknüpft werden können, und fast portiert wurde jede Systemarchitektur. Aber C ist als tragbare Assemblersprache nur bis zu einem gewissen Punkt nützlich. Es war nicht für diesen speziellen Zweck konzipiert.

Im Gegensatz dazu wurde das IR von LLVM von Anfang an als tragbare Baugruppe konzipiert. Eine Möglichkeit, diese Portabilität zu erreichen, besteht darin, Grundelemente unabhängig von einer bestimmten Maschinenarchitektur anzubieten. Beispielsweise sind Ganzzahltypen nicht auf die maximale Bitbreite der zugrunde liegenden Hardware beschränkt (z. B. 32 oder 64 Bit). Sie können primitive Ganzzahltypen mit so vielen Bits wie nötig erstellen, z. B. eine 128-Bit-Ganzzahl. Sie müssen sich auch nicht darum kümmern, die Ausgabe so zu gestalten, dass sie dem Befehlssatz eines bestimmten Prozessors entspricht. LLVM kümmert sich auch für Sie darum.

Das architekturneutrale Design von LLVM erleichtert die Unterstützung von Hardware aller Art in Gegenwart und Zukunft. Beispielsweise hat IBM kürzlich Code zur Unterstützung von z / OS, Linux on Power (einschließlich Unterstützung für die MASS-Vektorisierungsbibliothek von IBM) und AIX-Architekturen für die C-, C ++ - und Fortran-Projekte von LLVM bereitgestellt. 

Wenn Sie Live-Beispiele für LLVM IR sehen möchten, besuchen Sie die ELLCC Project-Website und probieren Sie die Live-Demo aus, die C-Code direkt im Browser in LLVM IR konvertiert.

Wie Programmiersprachen LLVM verwenden

Der häufigste Anwendungsfall für LLVM ist der AOT-Compiler (Ahead-of-Time) für eine Sprache. Beispielsweise kompiliert das Clang-Projekt C und C ++ vorab in native Binärdateien. LLVM macht aber auch andere Dinge möglich.

Just-in-Time-Kompilierung mit LLVM

In einigen Situationen muss Code zur Laufzeit im laufenden Betrieb generiert und nicht vorab kompiliert werden. Die Julia-Sprache zum Beispiel kompiliert JIT-Code, da sie schnell ausgeführt werden und über eine REPL (Read-Eval-Print-Schleife) oder eine interaktive Eingabeaufforderung mit dem Benutzer interagieren muss. 

Numba, ein Mathematik-Beschleunigungspaket für Python, kompiliert JIT-ausgewählte Python-Funktionen zu Maschinencode. Es kann auch Numba-dekorierten Code im Voraus kompilieren, aber (wie Julia) Python bietet eine schnelle Entwicklung, indem es eine interpretierte Sprache ist. Die Verwendung der JIT-Kompilierung zur Erstellung eines solchen Codes ergänzt den interaktiven Workflow von Python besser als die vorzeitige Kompilierung.

Andere experimentieren mit neuen Möglichkeiten, LLVM als JIT zu verwenden, z. B. dem Kompilieren von PostgreSQL-Abfragen, um die Leistung um das Fünffache zu steigern.

Automatische Codeoptimierung mit LLVM

LLVM kompiliert nicht nur die IR in nativen Maschinencode. Sie können es auch programmgesteuert steuern, um den Code während des gesamten Verknüpfungsprozesses mit einem hohen Grad an Granularität zu optimieren. Die Optimierungen können sehr aggressiv sein, einschließlich Inlining-Funktionen, Eliminieren von totem Code (einschließlich nicht verwendeter Typdeklarationen und Funktionsargumente) und Abrollen von Schleifen.

Auch hier liegt die Macht darin, all dies nicht selbst implementieren zu müssen. LLVM kann sie für Sie erledigen oder Sie können sie anweisen, sie nach Bedarf auszuschalten. Wenn Sie beispielsweise kleinere Binärdateien auf Kosten einer gewissen Leistung wünschen, kann Ihr Compiler-Frontend LLVM anweisen, das Abrollen der Schleife zu deaktivieren.

Domänenspezifische Sprachen mit LLVM

LLVM wurde verwendet, um Compiler für viele Allzwecksprachen zu erstellen, aber es ist auch nützlich, um Sprachen zu erstellen, die sehr vertikal oder exklusiv für eine Problemdomäne sind. In gewisser Weise leuchtet LLVM hier am hellsten, da es einen Großteil der Plackerei bei der Erstellung einer solchen Sprache beseitigt und eine gute Leistung erbringt.

Das Emscripten-Projekt verwendet beispielsweise LLVM-IR-Code und konvertiert ihn in JavaScript. Theoretisch kann jede Sprache mit einem LLVM-Backend Code exportieren, der im Browser ausgeführt werden kann. Langfristig ist geplant, LLVM-basierte Backends zu haben, die WebAssembly erstellen können. Emscripten ist jedoch ein gutes Beispiel dafür, wie flexibel LLVM sein kann.

Eine andere Möglichkeit, LLVM zu verwenden, besteht darin, einer vorhandenen Sprache domänenspezifische Erweiterungen hinzuzufügen. Nvidia verwendete LLVM, um den Nvidia CUDA-Compiler zu erstellen, mit dem Sprachen native Unterstützung für CUDA hinzufügen können, die als Teil des von Ihnen generierten nativen Codes kompiliert wird (schneller), anstatt über eine mitgelieferte Bibliothek aufgerufen zu werden (langsamer).

Der Erfolg von LLVM mit domänenspezifischen Sprachen hat neue Projekte innerhalb von LLVM vorangetrieben, um die von ihnen verursachten Probleme anzugehen. Das größte Problem ist, wie schwer es ist, einige DSLs ohne viel harte Arbeit am Frontend in LLVM-IR zu übersetzen. Eine in Arbeit befindliche Lösung ist das MLIR-Projekt Multi-Level Intermediate Representation (MLIR).

MLIR bietet bequeme Möglichkeiten zur Darstellung komplexer Datenstrukturen und Operationen, die dann automatisch in LLVM IR übersetzt werden können. Zum Beispiel könnte das TensorFlow-Framework für maschinelles Lernen viele seiner komplexen Datenflussdiagrammoperationen effizient mit MLIR zu nativem Code kompilieren lassen.

Arbeiten mit LLVM in verschiedenen Sprachen

Die typische Art, mit LLVM zu arbeiten, ist Code in einer Sprache, mit der Sie vertraut sind (und die natürlich die Bibliotheken von LLVM unterstützt).

Zwei gängige Sprachoptionen sind C und C ++. Viele LLVM-Entwickler verwenden aus mehreren guten Gründen standardmäßig einen dieser beiden: 

  • LLVM selbst ist in C ++ geschrieben.
  • Die APIs von LLVM sind in C- und C ++ - Inkarnationen verfügbar.
  • Viel Sprachentwicklung findet in der Regel mit C / C ++ als Basis statt

Dennoch sind diese beiden Sprachen nicht die einzigen Möglichkeiten. Viele Sprachen können nativ in C-Bibliotheken aufrufen, so dass es theoretisch möglich ist, eine LLVM-Entwicklung mit einer solchen Sprache durchzuführen. Es ist jedoch hilfreich, eine tatsächliche Bibliothek in der Sprache zu haben, die die LLVM-APIs elegant umschließt. Glücklicherweise haben viele Sprachen und Sprachlaufzeiten solche Bibliotheken, einschließlich C # /. NET / Mono, Rust, Haskell, OCAML, Node.js, Go und Python.

Eine Einschränkung ist, dass einige der Sprachbindungen an LLVM möglicherweise weniger vollständig sind als andere. Bei Python gibt es zum Beispiel viele Möglichkeiten, aber jede unterscheidet sich in ihrer Vollständigkeit und Nützlichkeit:

  • llvmlite, entwickelt von dem Team, das Numba erstellt, hat sich als aktueller Anwärter auf die Arbeit mit LLVM in Python herausgestellt. Es implementiert nur einen Teil der LLVM-Funktionalität, wie es die Anforderungen des Numba-Projekts vorschreiben. Diese Untergruppe bietet jedoch die überwiegende Mehrheit der Anforderungen von LLVM-Benutzern. (llvmlite ist im Allgemeinen die beste Wahl für die Arbeit mit LLVM in Python.)
  • Das LLVM-Projekt verwaltet seine eigenen Bindungen an die C-API von LLVM, diese werden jedoch derzeit nicht verwaltet.
  • llvmpy, die erste beliebte Python-Bindung für LLVM, wurde 2015 nicht mehr gewartet. Schlecht für jedes Softwareprojekt, aber schlechter bei der Arbeit mit LLVM, da in jeder LLVM-Ausgabe zahlreiche Änderungen vorgenommen wurden.
  • llvmcpy zielt darauf ab, die Python-Bindungen für die C-Bibliothek auf den neuesten Stand zu bringen, sie automatisiert auf dem neuesten Stand zu halten und sie mithilfe der nativen Python-Redewendungen zugänglich zu machen. llvmcpy befindet sich noch im Anfangsstadium, kann jedoch bereits einige rudimentäre Arbeiten mit den LLVM-APIs ausführen.

Wenn Sie neugierig sind, wie Sie LLVM-Bibliotheken zum Erstellen einer Sprache verwenden, haben die eigenen Ersteller von LLVM ein Tutorial mit C ++ oder OCAML, das Sie durch die Erstellung einer einfachen Sprache namens Kaleidoscope führt. Es wurde seitdem in andere Sprachen portiert:

  • Haskell:  Ein direkter Port des ursprünglichen Tutorials.
  • Python: Ein solcher Port folgt genau dem Tutorial, während der andere ein ehrgeizigeres Umschreiben mit einer interaktiven Befehlszeile ist. Beide verwenden llvmlite als Bindung an LLVM.
  • Rust  and  Swift: Es schien unvermeidlich, dass wir Ports des Tutorials für zwei der Sprachen erhalten, die LLVM ins Leben gerufen hat.

Schließlich ist das Tutorial auch in menschlichen Sprachen verfügbar  . Es wurde mit dem Original C ++ und Python ins Chinesische übersetzt.

Was LLVM nicht macht

Bei allem, was LLVM bietet, ist es nützlich zu wissen, was es nicht tut.

Beispielsweise analysiert LLVM die Grammatik einer Sprache nicht. Viele Tools wie Lex / Yacc, Flex / Bison, Lark und ANTLR erledigen diese Aufgabe bereits. Das Parsen soll sowieso von der Kompilierung entkoppelt werden, daher ist es nicht verwunderlich, dass LLVM nicht versucht, dies zu beheben.

LLVM befasst sich auch nicht direkt mit der größeren Kultur von Software in Bezug auf eine bestimmte Sprache. Installieren der Binärdateien des Compilers, Verwalten von Paketen in einer Installation und Aktualisieren der Toolkette - Sie müssen dies selbst tun.

Schließlich und vor allem gibt es immer noch gemeinsame Teile von Sprachen, für die LLVM keine Grundelemente bereitstellt. Viele Sprachen verfügen über eine Art Speicherverwaltung für Speicherbereinigungen, entweder als Hauptmethode zur Speicherverwaltung oder als Ergänzung zu Strategien wie RAII (die von C ++ und Rust verwendet werden). LLVM bietet Ihnen keinen Garbage-Collector-Mechanismus, bietet jedoch Tools zum Implementieren der Garbage-Collection, indem Code mit Metadaten markiert werden kann, die das Schreiben von Garbage-Collectors erleichtern.

Nichts davon schließt jedoch die Möglichkeit aus, dass LLVM eventuell native Mechanismen zum Implementieren der Garbage Collection hinzufügt. LLVM entwickelt sich schnell und wird etwa alle sechs Monate veröffentlicht. Und das Entwicklungstempo wird wahrscheinlich nur dank der Art und Weise zunehmen, wie viele aktuelle Sprachen LLVM in den Mittelpunkt ihres Entwicklungsprozesses gestellt haben.