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
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.
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ì myAdd
mostratu 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 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
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
Beni, lettori! Aduprate LLVM?
Source: www.habr.com