Ce este LLVM? Puterea din spatele Swift, Rust, Clang și multe altele

Limbajele noi și îmbunătățirile aduse celor existente se răspândesc în tot peisajul de dezvoltare. Rust de la Mozilla, Swift de la Apple, Kotlin de la Jetbrains și multe alte limbaje oferă dezvoltatorilor o nouă gamă de opțiuni pentru viteză, siguranță, comoditate, portabilitate și putere.

De ce acum? Un motiv important este reprezentat de noile instrumente pentru construirea de limbaje – în special, compilatoarele. Și principalul dintre acestea este LLVM, un proiect open source dezvoltat inițial de creatorul limbajului Swift, Chris Lattner, ca proiect de cercetare la Universitatea din Illinois.

LLVM facilitează nu numai crearea de noi limbaje, ci și îmbunătățirea dezvoltării celor existente. Acesta oferă instrumente pentru automatizarea multora dintre cele mai ingrate părți ale sarcinii de creare a unui limbaj: crearea unui compilator, portarea codului rezultat pe mai multe platforme și arhitecturi, generarea de optimizări specifice arhitecturii, cum ar fi vectorizarea, și scrierea de cod pentru a gestiona metaforele comune ale limbajului, cum ar fi excepțiile. Licența sa liberală înseamnă că poate fi reutilizat în mod liber ca o componentă software sau implementat ca serviciu.

Lista de limbaje care utilizează LLVM are multe nume cunoscute. Limbajul Swift de la Apple folosește LLVM ca și cadru de compilare, iar Rust folosește LLVM ca o componentă de bază a lanțului său de instrumente. De asemenea, multe compilatoare au o ediție LLVM, cum ar fi Clang, compilatorul C/C++ (de aici și numele, „C-lang”), el însuși un proiect strâns aliat cu LLVM. Mono, implementarea .NET, are o opțiune de compilare în cod nativ folosind un back-end LLVM. Iar Kotlin, în mod nominal un limbaj JVM, dezvoltă o versiune a limbajului numită Kotlin Native care folosește LLVM pentru a compila în cod nativ de mașină.

LLVM definit

În esența sa, LLVM este o bibliotecă pentru crearea programatică de cod nativ de mașină. Un dezvoltator folosește API-ul pentru a genera instrucțiuni într-un format numit reprezentare intermediară, sau IR. LLVM poate apoi să compileze IR într-un binar de sine stătător sau să efectueze o compilare JIT (just-in-time) a codului pentru a rula în contextul unui alt program, cum ar fi un interpretor sau un timp de execuție pentru limbajul respectiv.

API-urile LLVM oferă primitive pentru dezvoltarea multor structuri și modele comune întâlnite în limbajele de programare. De exemplu, aproape toate limbajele au conceptul de funcție și de variabilă globală, iar multe dintre ele au corutine și interfețe de funcții străine C. LLVM are funcții și variabile globale ca elemente standard în IR-ul său și are metafore pentru crearea de corutine și interfață cu bibliotecile C.

În loc să cheltuiți timp și energie pentru a reinventa aceste roți particulare, puteți folosi implementările LLVM și să vă concentrați pe părțile limbajului dumneavoastră care au nevoie de atenție.

IDG

Un exemplu de reprezentare intermediară (IR) a LLVM. În dreapta este un program simplu în C; în stânga este același cod tradus în LLVM IR de către compilatorul Clang.

LLVM: Proiectat pentru portabilitate

Pentru a înțelege LLVM, ar putea fi de ajutor să luăm în considerare o analogie cu limbajul de programare C: C este uneori descris ca fiind un limbaj de asamblare portabil, de nivel înalt, deoarece are construcții care pot fi corelate îndeaproape cu hardware-ul sistemului și a fost portat pe aproape toate arhitecturile de sistem. Dar C este util ca un limbaj de asamblare portabil doar până la un anumit punct; nu a fost proiectat pentru acest scop anume.

În schimb, IR al LLVM a fost proiectat de la început pentru a fi un limbaj de asamblare portabil. Unul dintre modurile în care realizează această portabilitate este prin oferirea de primitive independente de orice arhitectură de mașină particulară. De exemplu, tipurile de numere întregi nu sunt limitate la lățimea maximă de biți a hardware-ului de bază (cum ar fi 32 sau 64 de biți). Puteți crea tipuri primitive de numere întregi folosind oricâți biți sunt necesari, cum ar fi un număr întreg de 128 de biți. De asemenea, nu trebuie să vă faceți griji cu privire la crearea unei ieșiri care să se potrivească cu setul de instrucțiuni al unui procesor specific; LLVM are grijă și de acest lucru pentru dumneavoastră.

Proiectarea neutră din punct de vedere arhitectural a LLVM facilitează suportul pentru hardware de toate tipurile, prezente și viitoare. De exemplu, IBM a contribuit recent cu cod pentru a susține arhitecturile sale z/OS, Linux on Power (inclusiv suport pentru biblioteca de vectorizare MASS a IBM) și AIX pentru proiectele C, C++ și Fortran ale LLVM.

Dacă doriți să vedeți exemple live de LLVM IR, accesați site-ul web al proiectului ELLCC și încercați demonstrația live care convertește codul C în LLVM IR direct în browser.

Cum folosesc limbajele de programare LLVM

Cel mai comun caz de utilizare a LLVM este ca un compilator înainte de timp (AOT) pentru un limbaj. De exemplu, proiectul Clang compilează înainte de timp C și C++ în binare native. Dar LLVM face posibile și alte lucruri.

Compilarea just-in-time cu LLVM

Câteva situații necesită generarea codului din mers în timpul execuției, mai degrabă decât compilarea anticipată. Limbajul Julia, de exemplu, își compilează codul JIT, deoarece trebuie să ruleze rapid și să interacționeze cu utilizatorul prin intermediul unui REPL (buclă de citire-evaluare-imprimare) sau al unui prompt interactiv.

Numba, un pachet de accelerare matematică pentru Python, compilează JIT anumite funcții Python în cod mașină. Poate, de asemenea, să compileze din timp codul decorticat Numba, dar (ca și Julia) Python oferă o dezvoltare rapidă prin faptul că este un limbaj interpretat. Utilizarea compilării JIT pentru a produce un astfel de cod completează fluxul de lucru interactiv al Python mai bine decât compilarea înainte de timp.

Alții experimentează noi modalități de utilizare a LLVM ca JIT, cum ar fi compilarea interogărilor PostgreSQL, obținând o creștere de până la cinci ori a performanței.

IDG

Numba utilizează LLVM pentru a compila just-in-time codul numeric și a accelera execuția acestuia. Funcția sum2d accelerată JIT își termină execuția de aproximativ 139 de ori mai repede decât codul Python obișnuit.

Optimizarea automată a codului cu LLVM

LLVM nu compilează doar IR în cod mașină nativ. De asemenea, îl puteți dirija în mod programatic să optimizeze codul cu un grad ridicat de granularitate, pe tot parcursul procesului de legare. Optimizările pot fi destul de agresive, incluzând lucruri cum ar fi alinierea funcțiilor, eliminarea codului mort (inclusiv a declarațiilor de tip și a argumentelor funcțiilor nefolosite) și derularea buclelor.

Din nou, puterea constă în faptul că nu trebuie să implementați singur toate acestea. LLVM le poate gestiona pentru dvs. sau îi puteți indica să le dezactiveze în funcție de necesități. De exemplu, dacă doriți binare mai mici cu prețul unor performanțe, puteți cere front-end-ului compilatorului dumneavoastră să îi spună lui LLVM să dezactiveze derularea buclelor.

Limbaje specifice unui domeniu cu LLVM

LLVM a fost folosit pentru a produce compilatoare pentru multe limbaje de uz general, dar este, de asemenea, util pentru a produce limbaje care sunt foarte verticale sau exclusive unui domeniu de probleme. În unele privințe, acesta este locul unde LLVM strălucește cel mai mult, deoarece elimină o mare parte din corvoada creării unui astfel de limbaj și îl face să fie performant.

Proiectul Emscripten, de exemplu, ia codul LLVM IR și îl convertește în JavaScript, permițând, teoretic, oricărui limbaj cu un back-end LLVM să exporte cod care poate rula în browser. Planul pe termen lung este de a avea back-end-uri bazate pe LLVM care pot produce WebAssembly, dar Emscripten este un bun exemplu de cât de flexibil poate fi LLVM.

O altă modalitate prin care poate fi folosit LLVM este de a adăuga extensii specifice unui domeniu la un limbaj existent. Nvidia a folosit LLVM pentru a crea Nvidia CUDA Compiler, care permite limbajelor să adauge suport nativ pentru CUDA care se compilează ca parte a codului nativ pe care îl generați (mai rapid), în loc să fie invocat prin intermediul unei biblioteci livrate împreună cu acesta (mai lent).

Succesul lui LLVM cu limbajele specifice domeniului a stimulat noi proiecte în cadrul LLVM pentru a aborda problemele pe care acestea le creează. Cea mai mare problemă este modul în care unele DSL-uri sunt greu de tradus în LLVM IR fără multă muncă grea în partea frontală. O soluție în lucru este proiectul Multi-Level Intermediate Representation, sau MLIR.

MLIR oferă modalități convenabile de a reprezenta structuri de date și operații complexe, care pot fi apoi traduse automat în LLVM IR. De exemplu, cadrul de învățare automată TensorFlow ar putea avea multe dintre operațiile sale complexe de tip dataflow-graph compilate eficient în cod nativ cu MLIR.

Lucrul cu LLVM în diverse limbaje

Modul tipic de a lucra cu LLVM este prin cod într-un limbaj cu care vă simțiți confortabil (și care are suport pentru bibliotecile LLVM, bineînțeles).

Două alegeri comune de limbaje sunt C și C++. Mulți dezvoltatori LLVM optează în mod implicit pentru una dintre aceste două din mai multe motive bune:

  • LLVM însuși este scris în C++.
  • API-urile LLVM sunt disponibile în încarnări C și C++.
  • Multă dezvoltare de limbaj tinde să se întâmple cu C/C++ ca bază

Cu toate acestea, aceste două limbaje nu sunt singurele opțiuni. Multe limbaje pot apela nativ la bibliotecile C, astfel încât este teoretic posibil să se realizeze dezvoltarea LLVM cu orice astfel de limbaj. Dar ajută să aveți o bibliotecă reală în limbajul respectiv care să înfășoare în mod elegant API-urile LLVM. Din fericire, multe limbaje și timpii de execuție ai limbajelor au astfel de biblioteci, inclusiv C#/.NET/Mono, Rust, Haskell, OCAML, Node.js, Go și Python.

O singură observație este că unele dintre legăturile de limbaj pentru LLVM pot fi mai puțin complete decât altele. Cu Python, de exemplu, există multe opțiuni, dar fiecare variază în ceea ce privește completitudinea și utilitatea sa:

  • llvmlite, dezvoltat de echipa care creează Numba, a apărut ca fiind concurentul actual pentru a lucra cu LLVM în Python. Acesta implementează doar un subset al funcționalității LLVM, așa cum a fost dictat de nevoile proiectului Numba. Dar acest subset oferă marea majoritate a ceea ce au nevoie utilizatorii LLVM. (llvmlite este, în general, cea mai bună alegere pentru a lucra cu LLVM în Python.)
  • Proiectul LLVM își menține propriul set de legături pentru API-ul C al LLVM, dar acestea nu sunt în prezent întreținute.
  • llvmpy, prima legătură Python populară pentru LLVM, a ieșit din întreținere în 2015. Rău pentru orice proiect software, dar mai rău atunci când se lucrează cu LLVM, având în vedere numărul de modificări care apar în fiecare ediție a LLVM.
  • llvmcpy își propune să aducă la zi legăturile Python pentru biblioteca C, să le mențină actualizate într-un mod automatizat și să le facă accesibile folosind idiomurile native Python. llvmcpy se află încă în stadii incipiente, dar deja poate face unele lucrări rudimentare cu API-urile LLVM.

Dacă sunteți curioși să aflați cum să folosiți bibliotecile LLVM pentru a construi un limbaj, creatorii LLVM au un tutorial, folosind fie C++, fie OCAML, care vă ghidează prin crearea unui limbaj simplu numit Kaleidoscope. Acesta a fost de atunci portat și în alte limbaje:

  • Haskell: O portare directă a tutorialului original.
  • Python: Un astfel de port urmează îndeaproape tutorialul, în timp ce celălalt este o rescriere mai ambițioasă cu o linie de comandă interactivă. Ambele folosesc llvmlite ca legături cu LLVM.
  • Rust și Swift: Părea inevitabil să obținem portări ale tutorialului în două dintre limbajele pe care LLVM a ajutat să le aducă la existență.

În cele din urmă, tutorialul este disponibil și în limbaje umane. Acesta a fost tradus în chineză, folosind originalul C++ și Python.

Ce nu face LLVM

Cu tot ceea ce oferă LLVM, este util să știm și ce nu face.

De exemplu, LLVM nu analizează gramatica unei limbi. Multe instrumente fac deja această treabă, cum ar fi lex/yacc, flex/bison, Lark și ANTLR. Parsarea este oricum menită să fie decuplabilă de compilare, așa că nu este surprinzător că LLVM nu încearcă să abordeze nimic din toate acestea.

LLVM nu abordează, de asemenea, în mod direct cultura mai largă de software din jurul unui anumit limbaj. Instalarea binarelor compilatorului, gestionarea pachetelor într-o instalare și actualizarea lanțului de instrumente – trebuie să faceți asta pe cont propriu.

În cele din urmă, și cel mai important, există încă părți comune ale limbajelor pentru care LLVM nu oferă primitive. Multe limbaje au un fel de gestionare a memoriei cu colectare de gunoaie, fie ca modalitate principală de gestionare a memoriei, fie ca adjuvant la strategii precum RAII (pe care le folosesc C++ și Rust). LLVM nu vă oferă un mecanism de colectare a gunoiului, dar oferă instrumente de implementare a colectării gunoiului, permițând marcarea codului cu metadate care facilitează scrierea colectorilor de gunoi.

Nimic din toate acestea nu exclude însă posibilitatea ca LLVM să adauge în cele din urmă mecanisme native pentru implementarea colectării gunoiului. LLVM se dezvoltă rapid, cu o versiune majoră la fiecare șase luni sau cam așa ceva. Și este probabil ca ritmul de dezvoltare să se accelereze doar datorită modului în care multe limbaje actuale au pus LLVM în centrul procesului lor de dezvoltare.

Lasă un răspuns

Adresa ta de email nu va fi publicată.