Kompilatora izstrÄde ir ļoti grÅ«ts uzdevums. Bet, par laimi, attÄ«stot tÄdus projektus kÄ LLVM, Ŕīs problÄmas risinÄjums ir ievÄrojami vienkÄrÅ”ots, kas ļauj pat vienam programmÄtÄjam izveidot jaunu valodu, kas pÄc veiktspÄjas ir tuva C. Darbu ar LLVM sarežģī fakts, ka Å”is sistÄma ir attÄlota ar milzÄ«gu koda daudzumu, kas aprÄ«kots ar nelielu dokumentÄciju. Lai mÄÄ£inÄtu labot Å”o trÅ«kumu, materiÄla, kura tulkojumu mÄs Å”odien publicÄjam, autors demonstrÄs Go rakstÄ«tÄ koda piemÄrus un parÄdÄ«s, kÄ tie pirmo reizi tiek tulkoti
Pirmais piemÄrs
PirmÄ funkcija, ko es Å”eit apskatÄ«Å”u, ir vienkÄrÅ”s skaitļu pievienoÅ”anas mehÄnisms:
func myAdd(a, b int) int{
return a + b
}
Å Ä« funkcija ir ļoti vienkÄrÅ”a, un, iespÄjams, nekas nevar bÅ«t vienkÄrÅ”Äks. Tas tiek tulkots Å”ÄdÄ Go SSA kodÄ:
func myAdd(a int, b int) int:
entry:
t0 = a + b int
return t0
Å ajÄ skatÄ datu tipu padomi tiek novietoti labajÄ pusÄ, un vairumÄ gadÄ«jumu tos var ignorÄt.
Å is nelielais piemÄrs jau ļauj saskatÄ«t viena SSA aspekta bÅ«tÄ«bu. Proti, pÄrvÄrÅ”ot kodu SSA formÄ, katra izteiksme tiek sadalÄ«ta elementÄrÄkajÄs daļÄs, no kurÄm tÄ sastÄv. MÅ«su gadÄ«jumÄ komanda return a + b
, patiesÄ«bÄ, ir divas darbÄ«bas: divu skaitļu pievienoÅ”ana un rezultÄta atgrieÅ”ana.
TurklÄt Å”eit jÅ«s varat redzÄt programmas pamata blokus, Å”ajÄ kodÄ ir tikai viens bloks - ievades bloks. TÄlÄk mÄs runÄsim vairÄk par blokiem.
Go SSA kods viegli pÄrvÄrÅ”as par LLVM IR:
define i64 @myAdd(i64 %a, i64 %b) {
entry:
%0 = add i64 %a, %b
ret i64 %0
}
Var pamanÄ«t, ka, lai gan Å”eit tiek izmantotas dažÄdas sintaktiskÄs struktÅ«ras, funkcijas struktÅ«ra bÅ«tÄ«bÄ nemainÄs. LLVM IR kods ir nedaudz spÄcÄ«gÄks par Go SSA kodu, lÄ«dzÄ«gi kÄ C. Å eit funkcijas deklarÄcijÄ vispirms ir aprakstÄ«ts datu tips, kuru tas atgriež, argumenta tips ir norÄdÄ«ts pirms argumenta nosaukuma. TurklÄt, lai vienkÄrÅ”otu IS parsÄÅ”anu, pirms globÄlo entÄ«tiju nosaukumiem ir simbols @
, un pirms vietÄjiem nosaukumiem ir simbols %
(funkcija tiek uzskatÄ«ta arÄ« par globÄlu entÄ«tiju).
Viena lieta, kas jÄÅem vÄrÄ Å”ajÄ kodÄ, ir Go tipa reprezentÄcijas lÄmums int
, ko var attÄlot kÄ 32 bitu vai 64 bitu vÄrtÄ«bu atkarÄ«bÄ no kompilatora un kompilÄcijas mÄrÄ·a, tiek pieÅemts, kad LLVM Ä£enerÄ IR kodu. Tas ir viens no daudzajiem iemesliem, kÄpÄc LLVM IR kods nav, kÄ daudzi cilvÄki domÄ, no platformas neatkarÄ«gs. Å Ädu kodu, kas izveidots vienai platformai, nevar vienkÄrÅ”i paÅemt un kompilÄt citai platformai (ja vien jÅ«s neesat piemÄrots Ŕīs problÄmas risinÄÅ”anai
VÄl viens interesants punkts, ko vÄrts atzÄ«mÄt, ir tas, ka veids i64
nav vesels skaitlis ar zÄ«mi: tas ir neitrÄls skaitļa zÄ«mes attÄlojuma ziÅÄ. AtkarÄ«bÄ no instrukcijas tas var attÄlot gan parakstÄ«tus, gan neparakstÄ«tus numurus. PievienoÅ”anas darbÄ«bas attÄlojuma gadÄ«jumÄ tam nav nozÄ«mes, tÄpÄc nav atŔķirÄ«bas darbÄ ar skaitļiem ar parakstÄ«tiem vai neparakstÄ«tiem numuriem. Å eit es vÄlos atzÄ«mÄt, ka C valodÄ mainÄ«gÄ ar zÄ«mi pÄrpildÄ«Å”ana noved pie nedefinÄtas darbÄ«bas, tÄpÄc Clang priekÅ”gals pievieno darbÄ«bai karodziÅu. nsw
(bez paraksta iesaiÅojuma), kas norÄda LLVM, ka tÄ var pieÅemt, ka pievienoÅ”ana nekad nepÄrplÅ«st.
Tas var bÅ«t svarÄ«gi dažÄm optimizÄcijÄm. PiemÄram, pievienojot divas vÄrtÄ«bas i16
32 bitu platformÄ (ar 32 bitu reÄ£istriem) pÄc pievienoÅ”anas ir nepiecieÅ”ama zÄ«mes paplaÅ”inÄÅ”anas darbÄ«ba, lai paliktu diapazonÄ i16
. Å Ä« iemesla dÄļ bieži vien ir efektÄ«vÄk veikt darbÄ«bas ar veseliem skaitļiem, pamatojoties uz maŔīnu reÄ£istra izmÄriem.
Tas, kas notiek tÄlÄk ar Å”o IR kodu, mÅ«s Å”obrÄ«d Ä«paÅ”i neinteresÄ. Kods tiek optimizÄts (bet tÄda vienkÄrÅ”a piemÄra kÄ mÅ«su gadÄ«jumÄ nekas netiek optimizÄts) un pÄc tam pÄrveidots maŔīnkodÄ.
Otrais piemÄrs
NÄkamais piemÄrs, ko mÄs apskatÄ«sim, bÅ«s nedaudz sarežģītÄks. Proti, mÄs runÄjam par funkciju, kas summÄ veselu skaitļu daļu:
func sum(numbers []int) int {
n := 0
for i := 0; i < len(numbers); i++ {
n += numbers[i]
}
return n
}
Å is kods tiek pÄrveidots par Å”Ädu Go SSA kodu:
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
Å eit jau var redzÄt vairÄk konstrukcijas, kas raksturÄ«gas koda attÄloÅ”anai SSA formÄ. IespÄjams, ka Ŕī koda acÄ«mredzamÄkÄ iezÄ«me ir fakts, ka nav strukturÄtu plÅ«smas vadÄ«bas komandu. Lai kontrolÄtu aprÄÄ·inu plÅ«smu, ir tikai nosacÄ«juma un beznosacÄ«juma lÄcieni, un, ja mÄs uzskatÄm Å”o komandu par komandu plÅ«smas kontrolei, tad atgrieÅ”anÄs komanda.
Faktiski Å”eit jÅ«s varat pievÄrst uzmanÄ«bu tam, ka programma nav sadalÄ«ta blokos, izmantojot cirtainus lencÄs (kÄ C valodu saimÄ). Tas ir sadalÄ«ts ar etiÄ·etÄm, kas atgÄdina montÄžas valodas, un tiek parÄdÄ«ts pamata bloku veidÄ. SSA pamata bloki tiek definÄti kÄ blakus esoÅ”as koda secÄ«bas, kas sÄkas ar etiÄ·eti un beidzas ar pamata bloka pabeigÅ”anas instrukcijÄm, piemÄram, - return
Šø jump
.
VÄl viena interesanta Ŕī koda detaļa ir attÄlota instrukcijÄ phi
. NorÄdÄ«jumi ir diezgan neparasti, un to izpratne var aizÅemt kÄdu laiku. atcerieties, ka myAdd
parÄdÄ«ts iepriekÅ”, bet nav piemÄrots sarežģītÄkÄm funkcijÄm, piemÄram, Å”ajÄ sadaÄ¼Ä apskatÄ«tajai funkcijai sum
. KonkrÄti, mainÄ«gie mainÄs cilpas izpildes laikÄ i
Šø n
.
SSA apiet ierobežojumu vienreiz pieŔķirt mainÄ«gÄs vÄrtÄ«bas, izmantojot tÄ saukto instrukciju phi
(tÄ nosaukums ir Åemts no grieÄ·u alfabÄta). Fakts ir tÄds, ka, lai SSA koda attÄlojums tiktu Ä£enerÄts tÄdÄm valodÄm kÄ C, jums ir jÄizmanto daži triki. Å Ä«s instrukcijas izsaukÅ”anas rezultÄts ir mainÄ«gÄ (i
vai n
), un kÄ tÄ parametri tiek izmantots pamata bloku saraksts. PiemÄram, apsveriet Å”o instrukciju:
t0 = phi [entry: 0:int, for.body: t6] #n
TÄ nozÄ«me ir Å”Äda: ja iepriekÅ”Äjais pamatbloks bija bloks entry
(ievade), tad t0
ir konstante 0
, un ja iepriekÅ”Äjais pamatbloks bija for.body
, tad jums ir jÄÅem vÄrtÄ«ba t6
no Ŕī bloka. Tas viss var Ŕķist diezgan noslÄpumaini, taÄu Å”is mehÄnisms ir tas, kas liek SSA darboties. No cilvÄka viedokļa tas viss padara kodu grÅ«ti saprotamu, taÄu fakts, ka katra vÄrtÄ«ba tiek pieŔķirta tikai vienu reizi, daudzu optimizÄciju padara daudz vienkÄrÅ”Äku.
Å
emiet vÄrÄ, ka, rakstot pats savu kompilatoru, jums parasti nebÅ«s jÄsaskaras ar Å”Äda veida lietÄm. Pat Clang neÄ£enerÄ visus Å”os norÄdÄ«jumus phi
, tas izmanto mehÄnismu alloca
(tas atgÄdina darbu ar parastajiem vietÄjiem mainÄ«gajiem). PÄc tam, palaižot, tiek izsaukta LLVM optimizÄcijas caurlaide alloca
pÄrveidots SSA formÄ. TomÄr TinyGo saÅem ievadi no Go SSA, kas Ärti jau ir pÄrveidota SSA formÄ.
VÄl viens aplÅ«kojamÄ starpposma koda fragmenta jauninÄjums ir tÄds, ka piekļuve ŔķÄluma elementiem pÄc indeksa tiek attÄlota kÄ adreses aprÄÄ·inÄÅ”anas operÄcija un iegÅ«tÄ rÄdÄ«tÄja atsauces atcelÅ”anas darbÄ«ba. Å eit jÅ«s varat redzÄt tieÅ”u konstantu pievienoÅ”anu IR kodam (piemÄram - 1:int
). PiemÄrÄ ar funkciju myAdd
Å”is nav izmantots. Tagad, kad Ŕīs funkcijas vairs nav pieejamas, apskatÄ«sim, kÄds kods kļūst, kad tas tiek pÄrveidots par LLVM IR formu:
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
}
Å eit, tÄpat kÄ iepriekÅ”, mÄs varam redzÄt to paÅ”u struktÅ«ru, kas ietver citas sintaktiskÄs struktÅ«ras. PiemÄram, zvanos phi
vÄrtÄ«bas un etiÄ·etes apmainÄ«tas. TomÄr Å”eit ir kaut kas, kam ir vÄrts pievÄrst Ä«paÅ”u uzmanÄ«bu.
SÄkumÄ Å”eit jÅ«s varat redzÄt pavisam citu funkcijas parakstu. LLVM neatbalsta slÄÅus, un tÄ rezultÄtÄ TinyGo kompilators, kas Ä£enerÄja Å”o starpkodu, sadalÄ«ja Ŕīs datu struktÅ«ras aprakstu daļÄs. Tas varÄtu attÄlot trÄ«s ŔķÄles elementus (ptr
, len
Šø cap
) kÄ struktÅ«ru (struct), taÄu to attÄloÅ”ana kÄ trÄ«s atseviŔķas entÄ«tijas ļauj veikt dažas optimizÄcijas. Citi kompilatori var attÄlot ŔķÄli citos veidos atkarÄ«bÄ no mÄrÄ·a platformas funkciju izsaukÅ”anas konvencijÄm.
VÄl viena interesanta Ŕī koda iezÄ«me ir instrukcijas izmantoÅ”ana getelementptr
(bieži saÄ«sinÄts kÄ GEP).
Å Ä« instrukcija darbojas ar rÄdÄ«tÄjiem un tiek izmantota, lai iegÅ«tu rÄdÄ«tÄju uz ŔķÄluma elementu. PiemÄram, salÄ«dzinÄsim to ar Å”Ädu kodu, kas rakstÄ«ts C:
int* sliceptr(int *ptr, int index) {
return &ptr[index];
}
Vai ar Ŕo ekvivalentu:
int* sliceptr(int *ptr, int index) {
return ptr + index;
}
VissvarÄ«gÄkais Å”eit ir instrukcijas getelementptr
neveic atsauces atcelÅ”anas darbÄ«bas. Tas tikai aprÄÄ·ina jaunu rÄdÄ«tÄju, pamatojoties uz esoÅ”o. To var uztvert kÄ norÄdÄ«jumus mul
Šø add
aparatÅ«ras lÄ«menÄ«. JÅ«s varat lasÄ«t vairÄk par GEP instrukcijÄm
VÄl viena interesanta Ŕī starpkoda Ä«paŔība ir instrukcijas izmantoÅ”ana icmp
. Å Ä« ir vispÄrÄ«ga instrukcija, ko izmanto, lai Ä«stenotu veselu skaitļu salÄ«dzinÄjumus. Å Ä«s instrukcijas rezultÄts vienmÄr ir tipa vÄrtÄ«ba i1
ā loÄ£iskÄ vÄrtÄ«ba. Å ajÄ gadÄ«jumÄ salÄ«dzinÄjums tiek veikts, izmantojot atslÄgvÄrdu slt
(parakstÄ«ts mazÄk nekÄ), jo mÄs salÄ«dzinÄm divus skaitļus, kurus iepriekÅ” attÄloja tips int
. Ja mÄs salÄ«dzinÄtu divus neparakstÄ«tus veselus skaitļus, tad mÄs izmantotu icmp
, un salÄ«dzinÄÅ”anÄ izmantotais atslÄgvÄrds bÅ«tu ult
. Lai salÄ«dzinÄtu peldoÅ”Ä komata skaitļus, tiek izmantota cita instrukcija, fcmp
, kas darbojas līdzīgi.
RezultÄti
Uzskatu, ka Å”ajÄ materiÄlÄ esmu apskatÄ«jis svarÄ«gÄkÄs LLVM IR iezÄ«mes. Protams, Å”eit ir daudz vairÄk. KonkrÄti, koda starpposma attÄlojums var saturÄt daudzas anotÄcijas, kas ļauj optimizÄcijas gaitÄ Åemt vÄrÄ noteiktas kompilatoram zinÄmas koda iezÄ«mes, kuras citÄdi nevar izteikt IR. PiemÄram, tas ir karogs inbounds
GEP instrukcijas vai karodziÅi nsw
Šø nuw
, ko var pievienot instrukcijÄm add
. Tas pats attiecas uz atslÄgvÄrdu private
, norÄdot optimizÄtÄjam, ka tÄ atzÄ«mÄtÄ funkcija netiks atsaukta Ärpus paÅ”reizÄjÄs kompilÄcijas vienÄ«bas. Tas ļauj veikt daudzas interesantas starpprocedÅ«ru optimizÄcijas, piemÄram, novÄrst neizmantotos argumentus.
VairÄk par LLVM varat lasÄ«t
CienÄ«jamie lasÄ«tÄji! Vai jÅ«s izmantojat LLVM?
Avots: www.habr.com