LLVM saka perspektif Go

Ngembangake compiler minangka tugas sing angel banget. Nanging, untunge, kanthi pangembangan proyek kaya LLVM, solusi kanggo masalah iki disederhanakake, sing ngidini malah programmer siji bisa nggawe basa anyar sing cedhak karo kinerja C. Nggarap LLVM rumit amarga kasunyatane iki. sistem diwakili dening jumlah gedhe saka kode, dilengkapi karo sethitik dokumentasi . Kanggo nyoba mbenerake kekurangan iki, penulis materi, terjemahan sing diterbitake saiki, bakal nduduhake conto kode sing ditulis ing Go lan nuduhake carane pisanan diterjemahake menyang Ayo SSA, lan banjur ing LLVM IR nggunakake compiler tinyGO. Kode Go SSA lan LLVM IR wis rada diowahi kanggo mbusak samubarang sing ora cocog karo panjelasan sing diwenehake ing kene, supaya panjelasan bisa dingerteni.

LLVM saka perspektif Go

Conto pisanan

Fungsi pisanan sing bakal dakdeleng yaiku mekanisme sing gampang kanggo nambah nomer:

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

Fungsi iki prasaja banget, lan, mbok menawa, ora ana sing luwih gampang. Iki nerjemahake menyang kode Go SSA ing ngisor iki:

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

Kanthi tampilan iki, pitunjuk jinis data diselehake ing sisih tengen lan bisa diabaikan ing pirang-pirang kasus.

Conto cilik iki wis ngidini sampeyan ndeleng inti saka siji aspek SSA. Yaiku, nalika ngowahi kode menyang wangun SSA, saben ekspresi dipérang dadi bagéan paling dhasar sing disusun. Ing kasus kita, printah return a + b, nyatane, nggantosi rong operasi: nambah rong nomer lan ngasilake asil.

Kajaba iku, ing kene sampeyan bisa ndeleng blok dhasar program, ing kode iki mung ana siji blok - blok entri. Kita bakal ngomong luwih akeh babagan blok ing ngisor iki.

Kode Go SSA gampang diowahi dadi LLVM IR:

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

Apa sampeyan bisa sok dong mirsani, sanajan struktur sintaksis beda digunakake ing kene, struktur fungsi kasebut ora owah. Kode IR LLVM luwih kuwat tinimbang kode Go SSA, padha karo C. Ing kene, ing deklarasi fungsi, pisanan ana katrangan jinis data sing bali, jinis argumen dituduhake sadurunge jeneng argumen. Kajaba iku, kanggo nyederhanakake parsing IR, jeneng entitas global didhisiki dening simbol @, lan sadurunge jeneng lokal ana simbol % (fungsi uga dianggep minangka entitas global).

Siji bab sing kudu dicathet babagan kode iki yaiku keputusan perwakilan jinis Go int, kang bisa dituduhake minangka nilai 32-dicokot utawa 64-dicokot, gumantung ing compiler lan target kompilasi, ditampa nalika LLVM ngasilake kode IR. Iki salah siji saka akeh alasan sing LLVM kode IR ora, minangka akeh wong mikir, platform independen. Kode kasebut, digawe kanggo siji platform, ora mung bisa dijupuk lan dikompilasi kanggo platform liyane (kajaba sampeyan cocok kanggo ngatasi masalah iki. kanthi ati-ati banget).

Titik menarik liyane sing kudu dicathet yaiku jinis kasebut i64 iku ora integer mlebu: iku netral ing syarat-syarat makili tandha nomer. Gumantung ing instruksi kasebut, bisa makili nomer sing ditandatangani lan sing ora ditandatangani. Ing kasus perwakilan operasi tambahan, iki ora dadi masalah, mula ora ana bedane kanggo nggarap nomer sing ditandatangani utawa ora ditandatangani. Ing kene aku pengin dicathet yen ing basa C, kebanjiran variabel integer sing ditandatangani ndadékaké prilaku sing ora ditemtokake, saéngga frontend Clang nambahake gendera ing operasi kasebut. nsw (ora ana bungkus mlebu), sing ngandhani LLVM manawa bisa nganggep manawa tambahan kasebut ora bakal kebanjiran.

Iki bisa uga penting kanggo sawetara optimasi. Contone, nambahake rong nilai i16 ing platform 32-dicokot (karo ndhaftar 32-dicokot) mbutuhake, sawise tambahan, operasi expansion tandha supaya tetep ing jangkoan i16. Amarga iki, asring luwih efisien kanggo nindakake operasi integer adhedhasar ukuran register mesin.

Apa sing kedadeyan sabanjure karo kode IR iki ora dadi kapentingan khusus kanggo kita saiki. Kode kasebut dioptimalake (nanging ing kasus conto prasaja kaya kita, ora ana sing dioptimalake) banjur diowahi dadi kode mesin.

Tuladha nomer loro

Conto sabanjure sing bakal kita deleng bakal luwih rumit. Yaiku, kita ngomong babagan fungsi sing nyimpulake irisan integer:

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

Kode iki diowahi dadi kode Go SSA ing ngisor iki:

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

Ing kene sampeyan bisa ndeleng luwih akeh konstruksi khas kanggo makili kode ing wangun SSA. Mbok menawa fitur sing paling jelas saka kode iki yaiku ora ana perintah kontrol aliran terstruktur. Kanggo ngontrol aliran petungan, ana mung saratipun lan unconditional mlumpat, lan, yen kita nimbang printah iki minangka printah kanggo ngontrol aliran, printah bali.

Nyatane, ing kene sampeyan bisa menehi perhatian marang kasunyatan manawa program kasebut ora dipérang dadi blok nggunakake penyonggo kriting (kaya ing kulawarga C basa). Iki dipérang dadi label, kaya basa rakitan, lan ditampilake ing wangun blok dhasar. Ing SSA, pamblokiran dhasar ditetepake minangka urutan kode sing cedhak karo label lan diakhiri karo instruksi ngrampungake blok dhasar, kayata - return и jump.

Rincian liyane sing menarik saka kode iki diwakili dening instruksi kasebut phi. Pandhuan kasebut rada ora biasa lan butuh sawetara wektu kanggo ngerti. inget, itu S.S.A. singkatan saka Static Single Assignment. Iki minangka perwakilan penengah saka kode sing digunakake dening kompiler, sing saben variabel diwenehi nilai mung sapisan. Iki apik kanggo nyebut fungsi prasaja kaya fungsi kita myAddkapacak ing ndhuwur, nanging ora cocok kanggo fungsi liyane Komplek kayata fungsi rembugan ing bagean iki sum. Utamane, variabel diganti sajrone eksekusi loop i и n.

SSA ngliwati watesan kanggo nemtokake nilai variabel yen nggunakake instruksi sing diarani phi (jenenge dijupuk saka aksara Yunani). Kasunyatane yaiku supaya perwakilan kode SSA digawe kanggo basa kaya C, sampeyan kudu nggunakake sawetara trik. Asil nelpon instruksi iki minangka nilai saiki variabel (i utawa n), lan dhaptar blok dhasar digunakake minangka paramèter. Contone, nimbang pandhuan iki:

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

Tegesipun kados ing ngandhap punika: manawi pamblokiran dhasar saderengipun awujud pamblokiran entry (input), banjur t0 punika pancet 0, lan yen pemblokiran dhasar sadurungé ana for.body, banjur sampeyan kudu njupuk nilai t6 saka blok iki. Iki kabeh bisa uga katon misterius, nanging mekanisme iki ndadekake SSA bisa digunakake. Saka perspektif manungsa, kabeh iki ndadekake kode angel dimangerteni, nanging kasunyatan manawa saben nilai ditugasake mung sapisan nggawe akeh optimasi luwih gampang.

Elinga yen sampeyan nulis kompiler dhewe, sampeyan biasane ora kudu ngatasi masalah kaya iki. Malah Clang ora ngasilake kabeh instruksi kasebut phi, iku nggunakake mekanisme alloca (iku kaya nggarap variabel lokal biasa). Banjur, nalika mbukak pass optimasi LLVM disebut mem2reg, instruksi alloca diowahi dadi wangun SSA. Nanging, TinyGo nampa input saka Go SSA, sing, kanthi gampang, wis diowahi dadi formulir SSA.

Inovasi liyane saka fragmen kode penengah sing dipikirake yaiku akses menyang unsur irisan kanthi indeks dituduhake ing wangun operasi ngitung alamat lan operasi dereferencing pointer sing diasilake. Ing kene sampeyan bisa ndeleng tambahan langsung saka konstanta menyang kode IR (contone - 1:int). Ing conto karo fungsi myAdd iki wis ora digunakake. Saiki kita wis entuk fitur kasebut, ayo goleki apa kode iki nalika diowahi dadi formulir 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
}

Ing kene, kaya sadurunge, kita bisa ndeleng struktur sing padha, sing kalebu struktur sintaksis liyane. Contone, ing telpon phi nilai lan label diganti. Nanging, ana sing kudu diwenehi perhatian khusus ing kene.

Kanggo miwiti, ing kene sampeyan bisa ndeleng tandha fungsi sing beda banget. LLVM ora ndhukung irisan-irisan, lan minangka asil, minangka optimasi, compiler TinyGo sing ngasilake kode penengah iki pamisah gambaran saka struktur data iki dadi bagéan. Bisa makili telung unsur irisan (ptr, len и cap) minangka struktur (struct), nanging makili minangka telung entitas kapisah ngidini kanggo sawetara optimizations. Kompiler liyane bisa uga makili irisan kanthi cara liya, gumantung saka konvensi panggilan fungsi platform target.

Fitur menarik liyane saka kode iki yaiku panggunaan instruksi kasebut getelementptr (asring disingkat GEP).

Instruksi iki dianggo karo penunjuk lan digunakake kanggo njupuk pointer menyang unsur irisan. Contone, ayo mbandhingake karo kode ing ngisor iki sing ditulis ing C:

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

Utawa kanthi padha karo iki:

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

Sing paling penting ing kene yaiku instruksi kasebut getelementptr ora nindakake operasi dereferencing. Iku mung ngetung pitunjuk anyar adhedhasar sing wis ana. Bisa dijupuk minangka instruksi mul и add ing tingkat hardware. Sampeyan bisa maca liyane babagan instruksi GEP kene.

Fitur menarik liyane saka kode penengah iki yaiku panggunaan instruksi kasebut icmp. Iki minangka instruksi tujuan umum sing digunakake kanggo ngetrapake perbandingan integer. Asil saka nglakokaké instruksi iki tansah Nilai saka jinis i1 - nilai logis. Ing kasus iki, perbandingan digawe nggunakake tembung kunci slt (mlebu kurang saka), awit kita mbandhingaké rong nomer sadurunge dituduhake dening jinis int. Yen kita mbandhingake rong integer sing ora ditandatangani, mula kita bakal nggunakake icmp, lan tembung kunci sing digunakake ing perbandingan yaiku ult. Kanggo mbandhingake angka floating point, instruksi liyane digunakake, fcmp, sing dianggo kanthi cara sing padha.

Hasil

Aku pracaya ing materi iki aku wis dijamin fitur paling penting saka LLVM IR. Mesthi, ana luwih akeh ing kene. Utamane, perwakilan penengah saka kode bisa ngemot akeh anotasi sing ngidini optimasi pass kanggo njupuk menyang akun fitur tartamtu saka kode dikenal kanggo compiler sing ora bisa ditulis ing IR. Contone, iki gendéra inbounds instruksi GEP, utawa gendéra nsw и nuw, sing bisa ditambahake menyang instruksi add. Semono uga kanggo tembung kunci private, nuduhake pangoptimal yen fungsi sing ditandhani ora bakal dirujuk saka njaba unit kompilasi saiki. Iki ngidini akeh optimasi interprocedural sing menarik kaya ngilangi argumen sing ora digunakake.

Sampeyan bisa maca liyane babagan LLVM ing dokumentasi, sing bakal kerep dirujuk nalika ngembangake kompiler adhedhasar LLVM dhewe. kene nuntun, kang katon ing ngembangaken compiler kanggo basa banget prasaja. Loro-lorone sumber informasi iki bakal migunani kanggo sampeyan nalika nggawe compiler sampeyan dhewe.

Para pamaca ingkang kinurmatan! Apa sampeyan nggunakake LLVM?

LLVM saka perspektif Go

Source: www.habr.com

Add a comment