Cos'è LLVM? Il potere dietro Swift, Rust, Clang e altro ancora

Nuove lingue e miglioramenti rispetto a quelli esistenti si stanno moltiplicando in tutto il panorama in via di sviluppo. Rust di Mozilla, Swift di Apple, Kotlin di Jetbrains e molti altri linguaggi forniscono agli sviluppatori una nuova gamma di scelte in termini di velocità, sicurezza, praticità, portabilità e potenza.

Perché ora? Uno dei motivi principali sono i nuovi strumenti per la creazione di linguaggi, in particolare i compilatori. E il principale tra questi è LLVM, un progetto open source originariamente sviluppato dal creatore del linguaggio Swift Chris Lattner come progetto di ricerca presso l'Università dell'Illinois.

LLVM rende più facile non solo creare nuovi linguaggi, ma anche migliorare lo sviluppo di quelli esistenti. Fornisce strumenti per automatizzare molte delle parti più ingrate dell'attività di creazione del linguaggio: creazione di un compilatore, porting del codice emesso su più piattaforme e architetture, generazione di ottimizzazioni specifiche dell'architettura come la vettorizzazione e scrittura di codice per gestire metafore del linguaggio comune come eccezioni. La sua licenza liberale significa che può essere liberamente riutilizzato come componente software o distribuito come servizio.

L'elenco delle lingue che fanno uso di LLVM ha molti nomi familiari. Il linguaggio Swift di Apple utilizza LLVM come framework del compilatore e Rust utilizza LLVM come componente principale della sua catena di strumenti. Inoltre, molti compilatori hanno un'edizione LLVM, come Clang, il compilatore C / C ++ (questo il nome, "C-lang"), esso stesso un progetto strettamente alleato con LLVM. Mono, l'implementazione .NET, ha un'opzione per la compilazione in codice nativo utilizzando un back-end LLVM. E Kotlin, nominalmente un linguaggio JVM, sta sviluppando una versione del linguaggio chiamato Kotlin Native che utilizza LLVM per la compilazione in codice nativo della macchina.

Definito LLVM

Fondamentalmente, LLVM è una libreria per la creazione programmatica di codice nativo della macchina. Uno sviluppatore utilizza l'API per generare istruzioni in un formato chiamato rappresentazione intermedia o IR. LLVM può quindi compilare l'IR in un binario autonomo o eseguire una compilazione JIT (just-in-time) sul codice da eseguire nel contesto di un altro programma, come un interprete o un runtime per la lingua.

Le API di LLVM forniscono le primitive per lo sviluppo di molte strutture e modelli comuni trovati nei linguaggi di programmazione. Ad esempio, quasi tutte le lingue hanno il concetto di una funzione e di una variabile globale, e molte hanno coroutine e interfacce di funzioni esterne C. LLVM ha funzioni e variabili globali come elementi standard nel suo IR e ha metafore per creare coroutine e interfacciarsi con le librerie C.

Invece di spendere tempo ed energia per reinventare quelle ruote particolari, puoi semplicemente usare le implementazioni di LLVM e concentrarti sulle parti del tuo linguaggio che richiedono attenzione.

Ulteriori informazioni su Go, Kotlin, Python e Rust 

Partire:

  • Tocca la potenza della lingua Go di Google
  • I migliori IDE ed editor di lingue Go

Kotlin:

  • Cos'è Kotlin? Spiegata l'alternativa Java
  • Framework Kotlin: un'indagine sugli strumenti di sviluppo JVM

Pitone:

  • Cos'è Python? Tutto quello che devi sapere
  • Tutorial: come iniziare con Python
  • 6 librerie essenziali per ogni sviluppatore Python

Ruggine:

  • Cos'è la ruggine? Il modo per sviluppare software sicuro, veloce e facile
  • Scopri come iniziare con Rust 

LLVM: progettato per la portabilità

Per comprendere LLVM, potrebbe essere utile considerare un'analogia con il linguaggio di programmazione C: C è talvolta descritto come un linguaggio assembly portatile di alto livello, perché ha costruzioni che possono essere mappate strettamente all'hardware del sistema, ed è stato portato a quasi ogni architettura di sistema. Ma C è utile come linguaggio assembly portabile solo fino a un certo punto; non è stato progettato per quello scopo particolare.

Al contrario, l'IR di LLVM è stato progettato fin dall'inizio per essere un gruppo portatile. Un modo per ottenere questa portabilità è offrire primitive indipendenti da qualsiasi particolare architettura della macchina. Ad esempio, i tipi interi non sono limitati alla larghezza di bit massima dell'hardware sottostante (come 32 o 64 bit). È possibile creare tipi interi primitivi utilizzando tutti i bit necessari, come un numero intero a 128 bit. Inoltre, non devi preoccuparti di creare l'output in modo che corrisponda al set di istruzioni di un processore specifico; LLVM si prende cura di questo anche per te.

Il design indipendente dall'architettura di LLVM semplifica il supporto di hardware di tutti i tipi, presenti e futuri. Ad esempio, IBM ha recentemente contribuito al codice per supportare z / OS, Linux on Power (incluso il supporto per la libreria di vettorizzazione MASS di IBM) e architetture AIX per i progetti C, C ++ e Fortran di LLVM. 

Se vuoi vedere esempi dal vivo di LLVM IR, vai al sito Web del progetto ELLCC e prova la demo live che converte il codice C in LLVM IR direttamente nel browser.

Come i linguaggi di programmazione utilizzano LLVM

Il caso d'uso più comune per LLVM è come compilatore di anticipo (AOT) per una lingua. Ad esempio, il progetto Clang in anticipo compila C e C ++ in binari nativi. Ma LLVM rende possibili anche altre cose.

Compilazione just-in-time con LLVM

Alcune situazioni richiedono che il codice venga generato al volo in fase di esecuzione, piuttosto che compilato in anticipo. Il linguaggio Julia, ad esempio, JIT-compila il suo codice, perché deve essere eseguito velocemente e interagire con l'utente tramite un REPL (ciclo di lettura-valutazione-stampa) o un prompt interattivo. 

Numba, un pacchetto di accelerazione matematica per Python, JIT-compila funzioni Python selezionate in codice macchina. Può anche compilare codice decorato con Numba in anticipo, ma (come Julia) Python offre un rapido sviluppo essendo un linguaggio interpretato. L'uso della compilazione JIT per produrre tale codice integra il flusso di lavoro interattivo di Python meglio della compilazione anticipata.

Altri stanno sperimentando nuovi modi per utilizzare LLVM come JIT, come la compilazione di query PostgreSQL, ottenendo un aumento fino a cinque volte delle prestazioni.

Ottimizzazione automatica del codice con LLVM

LLVM non si limita a compilare l'IR in codice macchina nativo. Puoi anche indirizzarlo a livello di programmazione per ottimizzare il codice con un alto grado di granularità, durante tutto il processo di collegamento. Le ottimizzazioni possono essere piuttosto aggressive, comprese cose come l'inlining di funzioni, l'eliminazione di codice inattivo (comprese le dichiarazioni di tipo inutilizzate e gli argomenti di funzione) e lo srotolamento di loop.

Ancora una volta, il potere sta nel non dover implementare tutto questo da soli. LLVM può gestirli per te o puoi indicarli per disattivarli secondo necessità. Ad esempio, se vuoi binari più piccoli al costo di alcune prestazioni, potresti fare in modo che il front-end del compilatore dica a LLVM di disabilitare lo srotolamento del ciclo.

Linguaggi specifici del dominio con LLVM

LLVM è stato utilizzato per produrre compilatori per molti linguaggi generici, ma è anche utile per produrre linguaggi altamente verticali o esclusivi per un dominio problematico. In un certo senso, è qui che LLVM brilla di più, perché rimuove gran parte della fatica nella creazione di un tale linguaggio e lo fa funzionare bene.

Il progetto Emscripten, ad esempio, prende il codice IR LLVM e lo converte in JavaScript, in teoria consentendo a qualsiasi linguaggio con un back-end LLVM di esportare codice che può essere eseguito nel browser. Il piano a lungo termine è quello di avere back-end basati su LLVM in grado di produrre WebAssembly, ma Emscripten è un buon esempio di quanto possa essere flessibile LLVM.

Un altro modo in cui LLVM può essere utilizzato è aggiungere estensioni specifiche del dominio a una lingua esistente. Nvidia ha utilizzato LLVM per creare il compilatore Nvidia CUDA, che consente alle lingue di aggiungere il supporto nativo per CUDA che si compila come parte del codice nativo che stai generando (più velocemente), invece di essere invocato tramite una libreria fornita con esso (più lento).

Il successo di LLVM con linguaggi specifici del dominio ha stimolato nuovi progetti all'interno di LLVM per affrontare i problemi che creano. Il problema più grande è come alcuni DSL siano difficili da tradurre in LLVM IR senza molto duro lavoro sul front-end. Una soluzione in lavorazione è il progetto Multi-Level Intermediate Representation, o MLIR.

MLIR fornisce modi convenienti per rappresentare strutture e operazioni di dati complesse, che possono quindi essere tradotte automaticamente in LLVM IR. Ad esempio, il framework di machine learning TensorFlow potrebbe avere molte delle sue complesse operazioni di grafo del flusso di dati compilate in modo efficiente in codice nativo con MLIR.

Lavorare con LLVM in varie lingue

Il modo tipico per lavorare con LLVM è tramite codice in un linguaggio con cui ti senti a tuo agio (e che ha il supporto per le librerie di LLVM, ovviamente).

Due scelte di linguaggio comuni sono C e C ++. Molti sviluppatori LLVM utilizzano uno di questi due per diversi buoni motivi: 

  • Lo stesso LLVM è scritto in C ++.
  • Le API di LLVM sono disponibili nelle incarnazioni C e C ++.
  • La maggior parte dello sviluppo del linguaggio tende a verificarsi con C / C ++ come base

Tuttavia, quelle due lingue non sono le uniche scelte. Molti linguaggi possono chiamare nativamente nelle librerie C, quindi è teoricamente possibile eseguire lo sviluppo LLVM con qualsiasi linguaggio di questo tipo. Ma aiuta avere una libreria reale nel linguaggio che avvolge elegantemente le API di LLVM. Fortunatamente, molti linguaggi e runtime di linguaggio dispongono di tali librerie, tra cui C # /. NET / Mono, Rust, Haskell, OCAML, Node.js, Go e Python.

Un avvertimento è che alcuni dei collegamenti linguistici a LLVM potrebbero essere meno completi di altri. Con Python, ad esempio, ci sono molte scelte, ma ognuna varia nella sua completezza e utilità:

  • llvmlite, sviluppato dal team che crea Numba, è emerso come l'attuale concorrente per lavorare con LLVM in Python. Implementa solo un sottoinsieme delle funzionalità di LLVM, come dettato dalle esigenze del progetto Numba. Ma quel sottoinsieme fornisce la stragrande maggioranza di ciò di cui hanno bisogno gli utenti LLVM. (llvmlite è generalmente la scelta migliore per lavorare con LLVM in Python.)
  • Il progetto LLVM mantiene il proprio set di collegamenti all'API C di LLVM, ma attualmente non vengono mantenuti.
  • llvmpy, il primo popolare binding Python per LLVM, non è più in manutenzione nel 2015. Male per qualsiasi progetto software, ma peggio quando si lavora con LLVM, dato il numero di modifiche apportate in ogni edizione di LLVM.
  • llvmcpy mira ad aggiornare i collegamenti Python per la libreria C, a mantenerli aggiornati in modo automatizzato e a renderli accessibili utilizzando gli idiomi nativi di Python. llvmcpy è ancora nelle fasi iniziali, ma può già fare del lavoro rudimentale con le API LLVM.

Se sei curioso di sapere come utilizzare le librerie LLVM per creare un linguaggio, i creatori di LLVM hanno un tutorial, utilizzando C ++ o OCAML, che ti guida attraverso la creazione di un linguaggio semplice chiamato Kaleidoscope. Da allora è stato portato in altre lingue:

  • Haskell:  un porting diretto del tutorial originale.
  • Python: uno di questi port segue da vicino il tutorial, mentre l'altro è una riscrittura più ambiziosa con una riga di comando interattiva. Entrambi usano llvmlite come binding a LLVM.
  • Rust  and  Swift: sembrava inevitabile che avremmo portato il tutorial a due dei linguaggi che LLVM ha contribuito a portare all'esistenza.

Infine, il tutorial è disponibile anche in  lingue umane . È stato tradotto in cinese, utilizzando l'originale C ++ e Python.

Cosa non fa LLVM

Con tutto ciò che LLVM fornisce, è utile sapere anche cosa non fa.

Ad esempio, LLVM non analizza la grammatica di una lingua. Molti strumenti fanno già quel lavoro, come lex / yacc, flex / bison, Lark e ANTLR. L'analisi deve comunque essere disaccoppiata dalla compilazione, quindi non sorprende che LLVM non cerchi di affrontare nulla di tutto ciò.

LLVM inoltre non si rivolge direttamente alla più ampia cultura del software attorno a una data lingua. Installare i binari del compilatore, gestire i pacchetti in un'installazione e aggiornare la catena degli strumenti: è necessario farlo da soli.

Infine, e cosa più importante, ci sono ancora parti comuni di linguaggi per cui LLVM non fornisce primitive. Molti linguaggi hanno una qualche modalità di gestione della memoria raccolta dai rifiuti, sia come modo principale per gestire la memoria o come aggiunta a strategie come RAII (che C ++ e Rust usano). LLVM non fornisce un meccanismo di garbage collector, ma fornisce strumenti per implementare la garbage collection consentendo al codice di essere contrassegnato con metadati che facilitano la scrittura di garbage collector.

Niente di tutto questo, però, esclude la possibilità che LLVM possa eventualmente aggiungere meccanismi nativi per l'implementazione della garbage collection. LLVM si sta sviluppando rapidamente, con una versione principale ogni sei mesi circa. È probabile che il ritmo di sviluppo aumenti solo grazie al modo in cui molti linguaggi attuali hanno posto LLVM al centro del loro processo di sviluppo.