LLVM da una perspettiva Go

Sviluppà un compilatore hè un compitu assai difficiule. Ma, per furtuna, cù u sviluppu di prughjetti cum'è LLVM, a suluzione à stu prublema hè assai simplificata, chì permette ancu un solu programatore di creà una nova lingua chì hè vicinu in u rendiment à C. U travagliu cù LLVM hè cumplicatu da u fattu chì questu. sistema hè rapprisintatu da una quantità enorme di codice, dotatu di pocu documentazione. Per pruvà à curregà sta mancanza, l'autore di u materiale, a traduzzione di quale avemu publicatu oghje, hà da dimustrà esempi di codice scrittu in Go è mostra cumu si sò tradutti prima in Va SSA, è dopu in LLVM IR usendu u compilatore tinyGO. U codice Go SSA è LLVM IR hè statu ligeramente editatu per sguassà e cose chì ùn sò micca pertinenti à e spiegazioni date quì, per fà e spiegazioni più comprensibili.

LLVM da una perspettiva Go

Primu esempiu

A prima funzione chì aghju da vede quì hè un mecanismu simplice per aghjunghje numeri:

func myAdd(a, b int) int{
    return a + b
}

Sta funzione hè assai sèmplice, è, forsi, nunda puderia esse più simplice. Si traduce in u seguente codice Go SSA:

func myAdd(a int, b int) int:
entry:
    t0 = a + b                                                    int
    return t0

Cù sta vista, i suggerimenti di u tipu di dati sò posti à a diritta è ponu esse ignorati in a maiò parte di i casi.

Stu picculu esempiu permette digià di vede l'essenza di un aspettu di SSA. Vale à dì, quandu si cunvertisce u codice in a forma SSA, ogni espressione hè sparta in e parti più elementari di quale hè cumpostu. In u nostru casu, u cumandamentu return a + b, in fattu, rapprisenta duie operazioni: aghjunghje dui numeri è rinvià u risultatu.

Inoltre, quì pudete vede i blocchi basi di u prugramma; in questu codice ci hè solu un bloccu - u bloccu di entrata. Parleremu più nantu à i blocchi quì sottu.

U codice Go SSA si converte facilmente in LLVM IR:

define i64 @myAdd(i64 %a, i64 %b) {
entry:
  %0 = add i64 %a, %b
  ret i64 %0
}

Ciò chì pudete nutà hè chì ancu s'ellu si usanu diverse strutture sintattiche quì, a struttura di a funzione hè basicamente invariata. U codice LLVM IR hè un pocu più forte di u codice Go SSA, simili à C. Quì, in a dichjarazione di a funzione, prima ci hè una descrizzione di u tipu di dati chì torna, u tipu d'argumentu hè indicatu prima di u nome di l'argumentu. Inoltre, per simplificà l'analisi IR, i nomi di l'entità glubale sò preceduti da u simbulu @, è davanti à i nomi lucali ci hè un simbulu % (una funzione hè ancu cunsiderata una entità globale).

Una cosa da nutà di stu codice hè chì a decisione di rapprisintazioni di u tipu di Go int, chì pò esse rapprisintatu cum'è un valore 32-bit o 64-bit, secondu u compilatore è u scopu di a compilazione, hè accettatu quandu LLVM genera u codice IR. Questu hè unu di i tanti mutivi chì u codice LLVM IR ùn hè micca, cum'è parechje persone pensanu, indipendente da a piattaforma. Un tali codice, creatu per una piattaforma, ùn pò micca esse simplicemente pigliatu è compilatu per una altra piattaforma (salvo chì ùn site adattatu per risolve stu prublema. cun estrema cura).

Un altru puntu interessante da nutà hè chì u tipu i64 ùn hè micca un entero signatu: hè neutru in quantu à rapprisintà u signu di u numeru. Sicondu l'istruzzioni, pò rapprisintà numeri signati è senza signu. In u casu di a rapprisintazioni di l'operazione di l'aghjunzione, questu ùn importa micca, per quessa, ùn ci hè nisuna differenza in u travagliu cù numeri firmati o micca firmati. Quì mi piacerebbe nutà chì in a lingua C, overflowing una variabile intera firmata porta à un cumpurtamentu indefinitu, cusì u frontend Clang aghjunghjenu una bandiera à l'operazione. nsw (senza wrap firmatu), chì dice à LLVM chì pò suppone chì l'aghjuntu ùn hè mai overflow.

Questu pò esse impurtante per alcune ottimisazioni. Per esempiu, aghjunghje dui valori i16 su una piattaforma a 32 bit (cù registri a 32 bit) richiede, dopo l'aggiunta, una operazione di espansione di segni per rimanere in gamma i16. Per via di questu, hè spessu più efficaci per fà operazioni intere basate nantu à e dimensioni di u registru di a macchina.

Ciò chì succede dopu cù stu codice IR ùn hè micca d'interessu particulare per noi avà. U codice hè ottimizatu (ma in u casu di un esempiu simplice cum'è u nostru, nunda hè ottimizatu) è dopu cunvertitu in codice macchina.

Second esempiu

L'esempiu prossimu chì vedemu hè un pocu più cumplicatu. Vale à dì, parlemu di una funzione chì somma una fetta di numeri interi:

func sum(numbers []int) int {
    n := 0
    for i := 0; i < len(numbers); i++ {
        n += numbers[i]
    }
    return n
}

Stu codice converte à u seguente codice Go SSA:

func sum(numbers []int) int:
entry:
    jump for.loop
for.loop:
    t0 = phi [entry: 0:int, for.body: t6] #n                       int
    t1 = phi [entry: 0:int, for.body: t7] #i                       int
    t2 = len(numbers)                                              int
    t3 = t1 < t2                                                  bool
    if t3 goto for.body else for.done
for.body:
    t4 = &numbers[t1]                                             *int
    t5 = *t4                                                       int
    t6 = t0 + t5                                                   int
    t7 = t1 + 1:int                                                int
    jump for.loop
for.done:
    return t0

Quì pudete digià vede più custruzzioni tipica per rapprisintà u codice in a forma SSA. Forsi a funzione più ovvia di stu codice hè u fattu chì ùn ci sò micca cumandamenti di cuntrollu di flussu strutturatu. Per cuntrullà u flussu di i calculi, ci sò solu salti cundiziunali è incondizionati, è, se cunsideremu stu cumandamentu cum'è un cumandamentu per cuntrullà u flussu, un cumandamentu di ritornu.

In fatti, quì pudete attentu à u fattu chì u prugramma ùn hè micca divisu in blocchi cù curly braces (cum'è in a famiglia C di lingue). Hè divisu da etichette, reminiscente di lingue di assemblea, è prisentatu in forma di blocchi basi. In SSA, i blocchi basi sò definiti cum'è sequenze contigue di codice chì cumincianu cù una etichetta è finiscinu cù l'istruzzioni di cumpletu di bloccu basi, cum'è - return и jump.

Un altru dettagliu interessante di stu codice hè rapprisintatu da l'istruzzioni phi. L'istruzzioni sò abbastanza inusual è pò piglià un pocu di tempu per capiscenu. ricordati, chì SSA hè l'abbreviazione di Static Single Assignment. Questa hè una rapprisintazioni intermedia di u codice utilizatu da i compilatori, in quale ogni variabile hè attribuita un valore solu una volta. Questu hè grande per spressione funzioni simplici cum'è a nostra funzione myAddmostratu sopra, ma ùn hè micca adattatu per funzioni più cumplesse cum'è a funzione discutata in questa sezione sum. In particulare, variabili cambianu durante l'esekzione di u ciclu i и n.

SSA ignora a limitazione di l'assignazione di valori variabili una volta utilizendu una cusì chjamata struzzione phi (u so nome hè pigliatu da l'alfabetu grecu). U fattu hè chì per a rapprisintazioni SSA di codice per esse generata per lingue cum'è C, avete da ricurdà à certi trucchi. U risultatu di chjamà sta struzzione hè u valore attuale di a variàbile (i o n), è una lista di blocchi basi hè utilizatu cum'è i so paràmetri. Per esempiu, cunzidira sta struzzione:

t0 = phi [entry: 0:int, for.body: t6] #n

U so significatu hè u seguitu: se u bloccu di basa precedente era un bloccu entry (input), allora t0 hè una constante 0, è se u bloccu di basa precedente era for.body, allura vi tocca à piglià u valore t6 da stu bloccu. Tuttu chistu pò pari abbastanza misteriosu, ma questu mecanismu hè ciò chì face u travagliu SSA. Da una perspettiva umana, questu tuttu rende u codice difficiuli di capiscenu, ma u fattu chì ogni valore hè attribuitu solu una volta rende assai ottimisazioni assai più faciuli.

Nota chì sè vo scrivite u vostru propiu compilatore, di solitu ùn avete micca da trattà cun stu tipu di cose. Ancu Clang ùn genera micca tutte queste struzzioni phi, usa un mecanismu alloca (s'assumiglia à travaglià cù variabili lucali ordinali). Allora, quandu eseguite un passu di ottimisazione LLVM chjamatu mem2reg, instructions alloca cunvertitu à a forma SSA. TinyGo, però, riceve input da Go SSA, chì, convenientemente, hè digià cunvertitu in forma SSA.

Un'altra innuvazione di u frammentu di u codice intermediu in cunsiderà hè chì l'accessu à l'elementi di slice per indice hè rapprisintatu in a forma di una operazione di calculà l'indirizzu è una operazione di dereferenziazione di l'indicatore resultanti. Quì pudete vede l'aghjuntu direttu di custanti à u codice IR (per esempiu - 1:int). In l'esempiu cù a funzione myAdd questu ùn hè micca usatu. Avà chì avemu avutu queste caratteristiche fora di u modu, fighjemu un ochju à ciò chì stu codice diventa quandu hè cunvertitu in forma LLVM IR:

define i64 @sum(i64* %ptr, i64 %len, i64 %cap) {
entry:
  br label %for.loop

for.loop:                                         ; preds = %for.body, %entry
  %0 = phi i64 [ 0, %entry ], [ %5, %deref.next ]
  %1 = phi i64 [ 0, %entry ], [ %6, %deref.next ]
  %2 = icmp slt i64 %1, %len
  br i1 %2, label %for.body, label %for.done

for.body:                                         ; preds = %for.loop
  %3 = getelementptr i64, i64* %ptr, i64 %1
  %4 = load i64, i64* %3
  %5 = add i64 %0, %4
  %6 = add i64 %1, 1
  br label %for.loop

for.done:                                         ; preds = %for.loop
  ret i64 %0
}

Quì, cum'è prima, pudemu vede a listessa struttura, chì include altre strutture sintattiche. Per esempiu, in chjama phi valori è etichette scambiati. Tuttavia, ci hè qualcosa quì chì vale a pena prestu una attenzione particulari.

Per principià, quì pudete vede una signatura di funzione completamente diversa. LLVM ùn sustene micca fette, è in u risultatu, cum'è ottimisazione, u compilatore TinyGo chì hà generatu stu codice intermediu divide a descrizzione di sta struttura di dati in parti. Puderia rapprisintà trè elementi fette (ptr, len и cap) cum'è una struttura (struct), ma rapprisentanu cum'è trè entità separati permette alcune ottimisazioni. Altri compilatori ponu rapprisintà a fetta in altri modi, secondu e cunvenzioni di chjama di e funzioni di a piattaforma di destinazione.

Una altra caratteristica interessante di stu codice hè l'usu di l'istruzzioni getelementptr (spessu abbreviatu cum'è GEP).

Questa struzzione travaglia cù puntatori è hè aduprata per ottene un punteru à un elementu fetta. Per esempiu, paragunemu cù u seguente codice scrittu in C:

int* sliceptr(int *ptr, int index) {
    return &ptr[index];
}

O cù u seguente equivalente à questu:

int* sliceptr(int *ptr, int index) {
    return ptr + index;
}

U più impurtante quì hè chì l'istruzzioni getelementptr ùn eseguisce micca operazioni di dereferenziazione. Solu calcula un novu punteru basatu annantu à quellu esistenti. Pò esse pigliatu cum'è struzzioni mul и add à u livellu hardware. Pudete leghje più nantu à l'istruzzioni GEP ccà.

Un'altra caratteristica interessante di stu codice intermediu hè l'usu di l'istruzzioni icmp. Questa hè una struzzione generale utilizata per implementà paraguni interi. U risultatu di sta struzzione hè sempre un valore di tipu i1 - valore logicu. In questu casu, una comparazione hè fatta cù a keyword slt (firmatu menu di), postu chì avemu paragunatu dui numeri prima rapprisintati da u tipu int. Sè avemu paragunatu dui interi senza signu, allora avemu aduprà icmp, è a chjave utilizata in a paraguna seria ult. Per paragunà i numeri in virgule flottante, una altra struzzione hè aduprata, fcmp, chì travaglia in modu simili.

Risultati

Credu chì in questu materiale aghju cupertu e caratteristiche più impurtanti di LLVM IR. Di sicuru, ci hè assai di più quì. In particulare, a rapprisintazioni intermediata di u codice pò cuntene assai annotazioni chì permettenu passassi d'ottimisazione per piglià in contu certe caratteristiche di u codice cunnisciuti da u compilatore chì ùn pò micca esse altrimenti espressi in IR. Per esempiu, questu hè una bandiera inbounds Istruzzioni GEP, o bandiere nsw и nuw, chì pò esse aghjuntu à l'istruzzioni add. U stessu passa per a keyword private, indicà à l'ottimisatore chì a funzione chì marca ùn serà micca riferita da fora di l'unità di compilazione attuale. Questu permette assai ottimisazioni interprocedurali interessanti cum'è l'eliminazione di l'argumenti inutilizati.

Pudete leghje più nantu à LLVM in ducumentazione, chì vi riferite spessu quandu sviluppate u vostru propiu compilatore basatu in LLVM. Quì dirigenza, chì vede à sviluppà un compilatore per una lingua assai simplice. Tramindui sti fonti d'infurmazioni seranu utili per voi quandu creanu u vostru propiu compilatore.

Beni, lettori! Aduprate LLVM?

LLVM da una perspettiva Go

Source: www.habr.com

Add a comment