Wat is LLVM? De kracht achter Swift, Rust, Clang en meer

Nieuwe talen en verbeteringen van bestaande talen schieten als paddenstoelen uit de grond in het ontwikkelingslandschap. Mozilla's Rust, Apple's Swift, Jetbrains's Kotlin en vele andere talen bieden ontwikkelaars een nieuwe reeks keuzes voor snelheid, veiligheid, gemak, draagbaarheid en kracht.

Waarom nu? Een belangrijke reden zijn nieuwe tools voor het bouwen van talen, met name compilers. En de belangrijkste daarvan is LLVM, een open source-project dat oorspronkelijk werd ontwikkeld door Swift-taalontwikkelaar Chris Lattner als een onderzoeksproject aan de Universiteit van Illinois.

LLVM maakt het gemakkelijker om niet alleen nieuwe talen te creëren, maar ook om de ontwikkeling van bestaande talen te verbeteren. Het biedt tools voor het automatiseren van veel van de meest ondankbare delen van de taak van taalcreatie: het maken van een compiler, het porten van de uitgevoerde code naar meerdere platforms en architecturen, het genereren van architectuurspecifieke optimalisaties zoals vectorisatie, en het schrijven van code om veelgebruikte taalmetaforen zoals uitzonderingen. Dankzij de liberale licenties kan het vrijelijk worden hergebruikt als softwarecomponent of als service worden ingezet.

De lijst met talen die gebruik maken van LLVM heeft veel bekende namen. De Swift-taal van Apple gebruikt LLVM als compilerkader en Rust gebruikt LLVM als een kerncomponent van zijn gereedschapsketen. Ook hebben veel compilers een LLVM-editie, zoals Clang, de C / C ++ -compiler (dit is de naam, "C-lang"), zelf een project dat nauw verbonden is met LLVM. Mono, de .NET-implementatie, heeft een optie om te compileren naar native code met behulp van een LLVM-backend. En Kotlin, nominaal een JVM-taal, ontwikkelt een versie van de taal genaamd Kotlin Native die LLVM gebruikt om te compileren naar machine-native code.

LLVM gedefinieerd

In wezen is LLVM een bibliotheek voor het programmatisch maken van machine-native code. Een ontwikkelaar gebruikt de API om instructies te genereren in een formaat dat een tussenrepresentatie of IR wordt genoemd. LLVM kan vervolgens de IR compileren in een zelfstandig binair bestand of een JIT-compilatie (just-in-time) uitvoeren op de code die wordt uitgevoerd in de context van een ander programma, zoals een interpreter of runtime voor de taal.

De API's van LLVM bieden primitieven voor het ontwikkelen van veel voorkomende structuren en patronen die in programmeertalen voorkomen. Bijna elke taal heeft bijvoorbeeld het concept van een functie en van een globale variabele, en velen hebben coroutines en C-interfaces voor buitenlandse functies. LLVM heeft functies en globale variabelen als standaardelementen in zijn IR, en heeft metaforen voor het maken van coroutines en koppeling met C-bibliotheken.

In plaats van tijd en energie te besteden aan het opnieuw uitvinden van die specifieke wielen, kunt u gewoon de implementaties van LLVM gebruiken en u concentreren op de delen van uw taal die de aandacht nodig hebben.

Lees meer over Go, Kotlin, Python en Rust 

Gaan:

  • Maak gebruik van de kracht van de Go-taal van Google
  • De beste IDE's en editors in de Go-taal

Kotlin:

  • Wat is Kotlin? Het Java-alternatief uitgelegd
  • Kotlin-frameworks: een overzicht van JVM-ontwikkeltools

Python:

  • Wat is Python? Alles wat u moet weten
  • Zelfstudie: aan de slag met Python
  • 6 essentiële bibliotheken voor elke Python-ontwikkelaar

Roest:

  • Wat is roest? De manier om veilige, snelle en gemakkelijke softwareontwikkeling te doen
  • Leer hoe u aan de slag kunt met Rust 

LLVM: ontworpen voor draagbaarheid

Om LLVM te begrijpen, zou het kunnen helpen om een ​​analogie met de C-programmeertaal te overwegen: C wordt soms beschreven als een draagbare assembleertaal op hoog niveau, omdat het constructies heeft die nauw aansluiten bij de systeemhardware, en het is geport naar bijna elke systeemarchitectuur. Maar C is slechts tot op zekere hoogte bruikbaar als draagbare assembleertaal; het was niet ontworpen voor dat specifieke doel.

De IR van LLVM daarentegen is vanaf het begin ontworpen als een draagbare eenheid. Een manier waarop het deze overdraagbaarheid bereikt, is door primitieven aan te bieden die onafhankelijk zijn van een bepaalde machine-architectuur. De typen gehele getallen zijn bijvoorbeeld niet beperkt tot de maximale bitbreedte van de onderliggende hardware (zoals 32 of 64 bits). U kunt primitieve integer-typen maken met zoveel bits als nodig is, zoals een 128-bits geheel getal. U hoeft zich ook geen zorgen te maken over het vervaardigen van uitvoer die overeenkomt met de instructieset van een specifieke processor; LLVM regelt dat ook voor u.

Het architectuurneutrale ontwerp van LLVM maakt het gemakkelijker om allerlei soorten hardware te ondersteunen, nu en in de toekomst. IBM heeft bijvoorbeeld onlangs code bijgedragen ter ondersteuning van zijn z / OS, Linux on Power (inclusief ondersteuning voor IBM's MASS-vectorisatiebibliotheek) en AIX-architecturen voor de C-, C ++ - en Fortran-projecten van LLVM. 

Als je live voorbeelden van LLVM IR wilt zien, ga dan naar de ELLCC Project-website en probeer de live demo uit die C-code rechtstreeks in de browser omzet in LLVM IR.

Hoe programmeertalen LLVM gebruiken

De meest voorkomende use-case voor LLVM is als een AOT-compiler (AOT) voor een taal. Het Clang-project compileert bijvoorbeeld van tevoren C en C ++ naar native binaries. Maar LLVM maakt ook andere dingen mogelijk.

Just-in-time compileren met LLVM

In sommige situaties moet code direct tijdens runtime worden gegenereerd in plaats van van tevoren te worden gecompileerd. De Julia-taal, bijvoorbeeld, compileert JIT zijn code, omdat deze snel moet werken en moet communiceren met de gebruiker via een REPL (read-eval-print loop) of interactieve prompt. 

Numba, een wiskundig versnellingspakket voor Python, compileert geselecteerde Python-functies naar machinecode. Het kan ook van tevoren Numba-versierde code compileren, maar (net als Julia) Python biedt snelle ontwikkeling door een geïnterpreteerde taal te zijn. Het gebruik van JIT-compilatie om dergelijke code te produceren, vormt een betere aanvulling op de interactieve workflow van Python dan compilatie van tevoren.

Anderen experimenteren met nieuwe manieren om LLVM als een JIT te gebruiken, zoals het compileren van PostgreSQL-queries, waardoor de prestaties vervijfvoudigd worden.

Automatische code-optimalisatie met LLVM

LLVM compileert niet alleen de IR naar eigen machinecode. U kunt het ook programmatisch sturen om de code met een hoge mate van granulariteit te optimaliseren, helemaal tijdens het koppelingsproces. De optimalisaties kunnen behoorlijk agressief zijn, inclusief zaken als inlining-functies, het elimineren van dode code (inclusief ongebruikte type-declaraties en functie-argumenten) en het uitrollen van lussen.

Nogmaals, de kracht is dat je dit niet allemaal zelf hoeft te implementeren. LLVM kan ze voor u afhandelen, of u kunt het opdracht geven om ze indien nodig uit te schakelen. Als u bijvoorbeeld kleinere binaire bestanden wilt ten koste van enige prestatie, kunt u uw compiler-front-end LLVM laten vertellen om het uitrollen van de lus uit te schakelen.

Domeinspecifieke talen met LLVM

LLVM is gebruikt om compilers te maken voor veel algemene talen, maar het is ook nuttig voor het produceren van talen die zeer verticaal zijn of exclusief voor een probleemdomein. In sommige opzichten is LLVM hier het helderst, omdat het veel van de sleur bij het creëren van zo'n taal wegneemt en ervoor zorgt dat deze goed presteert.

Het Emscripten-project neemt bijvoorbeeld LLVM IR-code en converteert deze naar JavaScript, waardoor in theorie elke taal met een LLVM-backend code kan exporteren die in de browser kan worden uitgevoerd. Het langetermijnplan is om op LLVM gebaseerde backends te hebben die WebAssembly kunnen produceren, maar Emscripten is een goed voorbeeld van hoe flexibel LLVM kan zijn.

Een andere manier waarop LLVM kan worden gebruikt, is om domeinspecifieke extensies toe te voegen aan een bestaande taal. Nvidia gebruikte LLVM om de Nvidia CUDA Compiler te maken, waarmee talen native ondersteuning voor CUDA kunnen toevoegen die compileert als onderdeel van de native code die je genereert (sneller), in plaats van te worden aangeroepen via een meegeleverde bibliotheek (langzamer).

Het succes van LLVM met domeinspecifieke talen heeft nieuwe projecten binnen LLVM gestimuleerd om de problemen die ze veroorzaken aan te pakken. Het grootste probleem is dat sommige DSL's moeilijk te vertalen zijn naar LLVM IR zonder veel hard werk aan de voorkant. Een oplossing in de maak is het Multi-Level Intermediate Representation of MLIR-project.

MLIR biedt handige manieren om complexe gegevensstructuren en bewerkingen weer te geven, die vervolgens automatisch kunnen worden vertaald in LLVM IR. Het TensorFlow machine learning-framework zou bijvoorbeeld veel van zijn complexe dataflow-graph-bewerkingen efficiënt kunnen compileren naar native code met MLIR.

Werken met LLVM in verschillende talen

De typische manier om met LLVM te werken, is via code in een taal waarmee u vertrouwd bent (en die ondersteuning biedt voor de bibliotheken van LLVM natuurlijk).

Twee veelgebruikte taalkeuzes zijn C en C ++. Veel LLVM-ontwikkelaars gebruiken standaard een van deze twee om verschillende goede redenen: 

  • LLVM zelf is geschreven in C ++.
  • De API's van LLVM zijn beschikbaar in C- en C ++ - incarnaties.
  • Veel taalontwikkeling vindt plaats met C / C ++ als basis

Toch zijn die twee talen niet de enige keuzes. Veel talen kunnen native in C-bibliotheken worden gebeld, dus het is theoretisch mogelijk om LLVM-ontwikkeling uit te voeren met een dergelijke taal. Maar het helpt om een ​​echte bibliotheek te hebben in de taal die de API's van LLVM elegant verpakt. Gelukkig hebben veel talen en taalruntimes dergelijke bibliotheken, waaronder C # /. NET / Mono, Rust, Haskell, OCAML, Node.js, Go en Python.

Een voorbehoud is dat sommige taalbindingen met LLVM mogelijk minder volledig zijn dan andere. Met Python zijn er bijvoorbeeld veel keuzes, maar elk varieert in volledigheid en bruikbaarheid:

  • llvmlite, ontwikkeld door het team dat Numba heeft gemaakt, is naar voren gekomen als de huidige kanshebber om met LLVM in Python te werken. Het implementeert slechts een subset van de LLVM-functionaliteit, zoals bepaald door de behoeften van het Numba-project. Maar die subset biedt de overgrote meerderheid van wat LLVM-gebruikers nodig hebben. (llvmlite is over het algemeen de beste keuze om met LLVM in Python te werken.)
  • Het LLVM-project onderhoudt zijn eigen set bindingen met de C API van LLVM, maar deze worden momenteel niet onderhouden.
  • llvmpy, de eerste populaire Python-binding voor LLVM, viel uit onderhoud in 2015. Slecht voor elk softwareproject, maar erger bij het werken met LLVM, gezien het aantal wijzigingen dat in elke editie van LLVM optreedt.
  • llvmcpy streeft ernaar om de Python-bindingen voor de C-bibliotheek up-to-date te houden, ze op een geautomatiseerde manier up-to-date te houden en ze toegankelijk te maken met behulp van de native idiomen van Python. llvmcpy bevindt zich nog in de beginfase, maar kan al wat rudimentair werk doen met de LLVM API's.

Als je nieuwsgierig bent naar het gebruik van LLVM-bibliotheken om een ​​taal te bouwen, hebben de eigen makers van LLVM een tutorial, die C ++ of OCAML gebruikt, die je helpt bij het maken van een eenvoudige taal genaamd Kaleidoscope. Het is sindsdien geporteerd naar andere talen:

  • Haskell:  een directe port van de originele tutorial.
  • Python: Een van deze port volgt de tutorial nauwgezet, terwijl de andere een meer ambitieuze herschrijving is met een interactieve opdrachtregel. Beide gebruiken llvmlite als de bindingen met LLVM.
  • Rust  en  Swift: het leek onvermijdelijk dat we poorten van de tutorial zouden krijgen voor twee van de talen die LLVM hielp ontstaan.

Ten slotte is de tutorial ook beschikbaar in  menselijke talen. Het is in het Chinees vertaald met de originele C ++ en Python.

Wat LLVM niet doet

Met alles wat LLVM biedt, is het handig om ook te weten wat het niet doet.

LLVM ontleedt bijvoorbeeld de grammatica van een taal niet. Veel tools doen dat al, zoals lex / yacc, flex / bison, Lark en ANTLR. Parsing is hoe dan ook bedoeld om losgekoppeld te worden van de compilatie, dus het is niet verwonderlijk dat LLVM hier niets van probeert aan te pakken.

LLVM behandelt ook niet direct de grotere cultuur van software rond een bepaalde taal. Het installeren van de binaire bestanden van de compiler, het beheren van pakketten in een installatie en het upgraden van de gereedschapsketen - dat moet u zelf doen.

Ten slotte, en het belangrijkste, zijn er nog steeds gemeenschappelijke delen van talen waarvoor LLVM geen primitieven biedt. Veel talen hebben een soort van garbage-verzameld geheugenbeheer, hetzij als de belangrijkste manier om geheugen te beheren, hetzij als aanvulling op strategieën zoals RAII (die C ++ en Rust gebruiken). LLVM geeft je geen garbage-collector-mechanisme, maar het biedt wel tools om garbage collection te implementeren door code te markeren met metadata die het schrijven van garbage collectors gemakkelijker maakt.

Dit alles sluit echter de mogelijkheid uit dat LLVM uiteindelijk native mechanismen zou toevoegen voor het implementeren van garbage collection. LLVM ontwikkelt zich snel, met een grote release om de zes maanden. En het tempo van de ontwikkeling zal waarschijnlijk alleen maar toenemen dankzij de manier waarop veel huidige talen LLVM centraal hebben gesteld in hun ontwikkelingsproces.