Vad är LLVM? Kraften bakom Swift, Rust, Clang och mer

Nya språk och förbättringar av befintliga språk svampar genom hela det utvecklande landskapet. Mozillas Rust, Apples Swift, Jetbrains Kotlin och många andra språk ger utvecklare ett nytt utbud av val för hastighet, säkerhet, bekvämlighet, bärbarhet och kraft.

Varför nu? En stor anledning är nya verktyg för att bygga språk - specifikt kompilatorer. Och chefen bland dem är LLVM, ett projekt med öppen källkod som ursprungligen utvecklades av den snabba språkskaparen Chris Lattner som ett forskningsprojekt vid University of Illinois.

LLVM gör det lättare att inte bara skapa nya språk utan att förbättra utvecklingen av befintliga. Det ger verktyg för att automatisera många av de mest otacksamma delarna av uppgiften att skapa språk: skapa en kompilator, portera den utmatade koden till flera plattformar och arkitekturer, generera arkitekturspecifika optimeringar som vektorisering och skriva kod för att hantera vanliga språkmetaforer som undantag. Dess liberala licensiering innebär att den fritt kan återanvändas som en programvarukomponent eller distribueras som en tjänst.

Listan över språk som använder LLVM har många bekanta namn. Apples Swift-språk använder LLVM som kompilatorram och Rust använder LLVM som en kärnkomponent i sin verktygskedja. Många kompilatorer har också en LLVM-utgåva, såsom Clang, C / C ++ -kompilatorn (detta är namnet "C-lang"), i sig självt ett projekt nära allierat med LLVM. Mono, .NET-implementeringen, har ett alternativ att kompilera till inbyggd kod med en LLVM-backend. Och Kotlin, nominellt ett JVM-språk, utvecklar en version av språket som heter Kotlin Native som använder LLVM för att kompilera till maskininbyggd kod.

LLVM definierad

I sitt hjärta är LLVM ett bibliotek för att programmatiskt skapa maskininbyggd kod. En utvecklare använder API: t för att generera instruktioner i ett format som kallas en mellanrepresentation eller IR. LLVM kan sedan kompilera IR till en fristående binär eller utföra en JIT-kompilering (just-in-time) på koden som ska köras i samband med ett annat program, till exempel en tolk eller körtid för språket.

LLVM: s API: er ger primitiv för att utveckla många vanliga strukturer och mönster som finns i programmeringsspråk. Till exempel har nästan alla språk begreppet en funktion och en global variabel, och många har coroutines och C-gränssnitt för främmande funktioner. LLVM har funktioner och globala variabler som standardelement i sin IR, och har metaforer för att skapa coroutines och gränssnitt med C-bibliotek.

Istället för att spendera tid och energi på att uppfinna just dessa hjul kan du bara använda LLVMs implementeringar och fokusera på de delar av ditt språk som behöver uppmärksamhet.

Läs mer om Go, Kotlin, Python och Rust 

Gå:

  • Tryck på kraften i Googles Go-språk
  • De bästa Go-språket IDE: er och redaktörer

Kotlin:

  • Vad är Kotlin? Java-alternativet förklaras
  • Kotlin ramar: En undersökning av JVM-utvecklingsverktyg

Pytonorm:

  • Vad är Python? Allt du behöver veta
  • Självstudier: Hur man kommer igång med Python
  • 6 viktiga bibliotek för alla Python-utvecklare

Rost:

  • Vad är rost? Sättet att göra säker, snabb och enkel mjukvaruutveckling
  • Lär dig hur du kommer igång med Rust 

LLVM: Designad för bärbarhet

För att förstå LLVM kan det hjälpa att överväga en analogi med C-programmeringsspråket: C beskrivs ibland som ett bärbart monteringsspråk på hög nivå, eftersom det har konstruktioner som kan kartläggas nära systemhårdvaran, och det har portats till nästan varje systemarkitektur. Men C är användbart som ett bärbart monteringsspråk bara upp till en punkt; den var inte utformad för just det syftet.

Däremot designades LLVMs IR från början för att vara en bärbar enhet. Ett sätt att åstadkomma denna portabilitet är genom att erbjuda primitiva oberoende av någon speciell maskinarkitektur. Till exempel är heltalstyper inte begränsade till den maximala bitbredden för den underliggande hårdvaran (till exempel 32 eller 64 bitar). Du kan skapa primitiva heltalstyper med så många bitar som behövs, som ett 128-bitars heltal. Du behöver inte oroa dig för att skapa utdata för att matcha en specifik processorns instruktionsuppsättning. LLVM tar hand om det också för dig.

LLVMs arkitekturneutrala design gör det lättare att stödja hårdvara av alla slag, nu och i framtiden. Till exempel bidrog IBM nyligen med kod för att stödja sitt z / OS, Linux on Power (inklusive stöd för IBMs MASS-vektoriseringsbibliotek) och AIX-arkitekturer för LLVM: s C-, C ++- och Fortran-projekt. 

Om du vill se liveexempel på LLVM IR, gå till ELLCC Project-webbplatsen och prova live-demo som konverterar C-kod till LLVM IR direkt i webbläsaren.

Hur programmeringsspråk använder LLVM

Det vanligaste användningsfallet för LLVM är som en AOT-kompilator för ett språk. Exempelvis sammanställer Clang-projektet i förväg C och C ++ till inbyggda binära filer. Men LLVM gör också andra saker möjliga.

Just-in-time kompilering med LLVM

Vissa situationer kräver att kod genereras i farten vid körning, snarare än att kompileras i förväg. Juliaspråket, till exempel, JIT-sammanställer sin kod, eftersom det måste springa snabbt och interagera med användaren via en REPL (läs-eval-utskriftslinga) eller interaktiv promp. 

Numba, ett matematikaccelereringspaket för Python, sammanställer utvalda Python-funktioner till maskinkod. Det kan också sammanställa Numba-dekorerad kod i förväg, men (som Julia) erbjuder Python snabb utveckling genom att vara ett tolkat språk. Att använda JIT-kompilering för att producera sådan kod kompletterar Pythons interaktiva arbetsflöde bättre än kompilering i förväg.

Andra experimenterar med nya sätt att använda LLVM som en JIT, som att sammanställa PostgreSQL-frågor, vilket ger upp till en femfaldig prestationsökning.

Automatisk kodoptimering med LLVM

LLVM kompilerar inte bara IR till naturlig maskinkod. Du kan också programmera den för att optimera koden med en hög grad av granularitet, hela vägen genom länkningsprocessen. Optimeringarna kan vara ganska aggressiva, inklusive saker som att infoga funktioner, eliminera dödkod (inklusive oanvända typdeklarationer och funktionsargument) och rulla slingor.

Återigen är makten att inte behöva implementera allt detta själv. LLVM kan hantera dem åt dig, eller du kan rikta den för att slå av dem efter behov. Om du till exempel vill ha mindre binära filer på bekostnad av viss prestanda kan du låta din kompilatorgränssnitt berätta för LLVM att inaktivera looprullning.

Domänspecifika språk med LLVM

LLVM har använts för att producera kompilatorer för många allmänna språk, men det är också användbart för att producera språk som är mycket vertikala eller exklusiva för en problemdomän. På vissa sätt är det här som LLVM lyser ljusast, eftersom det tar bort mycket slitage för att skapa ett sådant språk och får det att fungera bra.

Emscripten-projektet tar till exempel LLVM IR-kod och konverterar den till JavaScript, i teorin så att alla språk med en LLVM-backend kan exportera kod som kan köras i webbläsaren. Den långsiktiga planen är att ha LLVM-baserade bakändar som kan producera WebAssemble, men Emscripten är ett bra exempel på hur flexibel LLVM kan vara.

Ett annat sätt LLVM kan användas är att lägga till domänspecifika tillägg till ett befintligt språk. Nvidia använde LLVM för att skapa Nvidia CUDA Compiler, som låter språk lägga till inbyggt stöd för CUDA som kompilerar som en del av den inbyggda koden du genererar (snabbare), istället för att åberopas genom ett bibliotek som levereras med det (långsammare).

LLVM: s framgång med domänspecifika språk har fått nya projekt inom LLVM att lösa problemen de skapar. Den största frågan är hur vissa DSL: er är svåra att översätta till LLVM IR utan mycket hårt arbete i fronten. En lösning i arbetet är Multi-Level Intermediate Representation, eller MLIR-projektet.

MLIR ger praktiska sätt att representera komplexa datastrukturer och operationer, som sedan kan översättas automatiskt till LLVM IR. Till exempel kan TensorFlow-maskininlärningsramverket ha många av dess komplexa dataflöde-grafoperationer effektivt sammanställt till inbyggd kod med MLIR.

Arbeta med LLVM på olika språk

Det typiska sättet att arbeta med LLVM är via kod på ett språk du är bekväm med (och det har naturligtvis stöd för LLVMs bibliotek).

Två vanliga språkval är C och C ++. Många LLVM-utvecklare är standard för en av dessa två av flera goda skäl: 

  • LLVM själv är skrivet i C ++.
  • LLVM: s API: er finns i C- och C ++ -inkarnationer.
  • Mycket språkutveckling tenderar att hända med C / C ++ som bas

Ändå är dessa två språk inte de enda valen. Många språk kan kallas in i C-bibliotek, så det är teoretiskt möjligt att utföra LLVM-utveckling med något sådant språk. Men det hjälper att ha ett verkligt bibliotek på språket som elegant omsluter LLVM: s API: er. Lyckligtvis har många språk och språketid sådana bibliotek, inklusive C # /. NET / Mono, Rust, Haskell, OCAML, Node.js, Go och Python.

En varning är att vissa av språkbindningarna till LLVM kan vara mindre kompletta än andra. Med Python finns det till exempel många val, men var och en varierar i dess fullständighet och användbarhet:

  • llvmlite, utvecklat av teamet som skapar Numba, har framstått som den nuvarande utmanaren för att arbeta med LLVM i Python. Den implementerar endast en delmängd av LLVMs funktionalitet, vilket dikteras av Numba-projektets behov. Men den delmängden ger den stora majoriteten av vad LLVM-användare behöver. (llvmlite är i allmänhet det bästa valet för att arbeta med LLVM i Python.)
  • LLVM-projektet behåller sin egen uppsättning bindningar till LLVMs C API, men de upprätthålls för närvarande inte.
  • llvmpy, den första populära Python-bindningen för LLVM, föll ur underhåll 2015. Dåligt för alla programvaruprojekt, men värre när man arbetar med LLVM, med tanke på antalet förändringar som följer i varje utgåva av LLVM.
  • llvmcpy syftar till att uppdatera Python-bindningarna för C-biblioteket, hålla dem uppdaterade på ett automatiserat sätt och göra dem tillgängliga med hjälp av Pythons ursprungliga idiom. llvmcpy är fortfarande i ett tidigt skede, men kan redan göra lite rudimentärt arbete med LLVM API: er.

Om du är nyfiken på hur du använder LLVM-bibliotek för att bygga ett språk, har LLVMs egna skapare en handledning, antingen C ++ eller OCAML, som leder dig genom att skapa ett enkelt språk som heter Kalejdoskop. Det har sedan portats till andra språk:

  • Haskell:  En direkt port av originalhandledningen.
  • Python: En sådan port följer självstudien noga, medan den andra är en mer ambitiös omskrivning med en interaktiv kommandorad. Båda dessa använder llvmlite som bindningar till LLVM.
  • Rust  och  Swift: Det verkade oundvikligt att vi skulle få portarna i handboken till två av de språk som LLVM hjälpte till.

Slutligen är självstudien tillgänglig på  mänskliga språk. Det har översatts till kinesiska med original C ++ och Python.

Vad LLVM inte gör

Med allt som LLVM tillhandahåller är det bra att också veta vad den inte gör.

Till exempel analyserar LLVM inte språkets grammatik. Många verktyg gör redan det jobbet, som lex / yacc, flex / bison, Lark och ANTLR. Parsing är tänkt att avkopplas från sammanställning ändå, så det är inte förvånande att LLVM inte försöker ta itu med något av detta.

LLVM tar inte heller direkt upp den större programvarukulturen kring ett visst språk. Installera kompilatorns binärer, hantera paket i en installation och uppgradera verktygskedjan - du måste göra det själv.

Slutligen, och viktigast av allt, finns det fortfarande vanliga delar av språk som LLVM inte ger primitiver för. Många språk har något sätt att hantera sopor i minnet, antingen som det viktigaste sättet att hantera minne eller som ett komplement till strategier som RAII (som C ++ och Rust använder). LLVM ger dig inte en sopsamlingsmekanism, men det ger verktyg för att implementera sopsamling genom att låta koden markeras med metadata som gör det lättare att skriva sopor.

Inget av detta utesluter dock möjligheten att LLVM så småningom kan lägga till inbyggda mekanismer för att genomföra skräpsamling. LLVM utvecklas snabbt, med en större release var sjätte månad. Och utvecklingshastigheten kommer sannolikt bara att öka tack vare hur många nuvarande språk har satt LLVM i centrum för deras utvecklingsprocess.