LLVM Go vaatenurgast

Kompilaatori arendamine on vĂ€ga raske ĂŒlesanne. Kuid Ă”nneks on projektide, nagu LLVM, elavdamisega see ĂŒlesanne oluliselt lihtsamaks muutunud, vĂ”imaldades isegi ĂŒksikprogrammeerijal luua uue keele, mis on jĂ”udluse poolest C-le lĂ€hedane. Töö LLVM-iga on keeruline, kuna see sĂŒsteem sisaldab tohutul hulgal koodi, millel on vĂ€he dokumentatsiooni. Selle puuduse parandamiseks kavatseb artikli autor, mille tĂ”lget tĂ€na avaldame, demonstreerida Go-s kirjutatud koodinĂ€iteid ja nĂ€idata, kuidas need kĂ”igepealt tĂ”lgitakse Go SSA, ja seejĂ€rel - LLVM IR-iks kasutades kompilaatorit TinyGO. Go SSA ja LLVM IR kood on veidi redigeeritud, eemaldatud on kĂ”ik, mis ei puuduta siin esitatud selgitusi, et need oleksid arusaadavamad.

LLVM Go vaatenurgast

Esimene nÀide

Esimene funktsioon, mida kavatseme siin analĂŒĂŒsida, on lihtne mehhanism numbrite liitmiseks:

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

See funktsioon on vÀga lihtne ja tÔenÀoliselt ei saaks midagi lihtsamat vÀlja mÔelda. See tÔlgitakse jÀrgnevaks Go SSA koodiks:

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

Sellise funktsiooni esituse korral on tĂŒĂŒpide vihjed paigutatud paremale; enamikul juhtudel ei pea nendele tĂ€helepanu pöörama.

See vĂ€ike nĂ€ide vĂ”imaldab juba nĂ€ha ĂŒhte SSA aspekti. Nimelt jagatakse koodi konverteerimisel SSA vormiks iga vĂ€ljend kĂ”ige elementaarsemateks osadeks, millest see koosneb. Meie puhul kĂ€sk return a + b, kujutab tegelikult endast kaht tehte: kahe numbri liitmist ja tulemuse tagastamist.

Lisaks sellele on siit nĂ€ha ka programmi baasklid, antud koodis on ainult ĂŒks blokk - sisendblokk (entry block). RÀÀgime blokkidest lĂ€hemalt hiljem.

Go SSA kood tÔlgitakse hÔlpsasti LLVM IR-iks:

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

On vĂ”ib mĂ€rgata, et kuigi siin kasutatakse teisi sĂŒntaktilisi konstruktsioone, on funktsiooni struktuur pĂ”himĂ”tteliselt muutumatu. LLVM IR kood on pisut vĂ”imsam kui Go SSA kood, sarnanedes C-le. Siin, funktsiooni deklareerimisel, jĂ€rgneb kĂ”igepealt kirjeldus sellele, millist andmetĂŒĂŒpi see tagastab, ja argumendi tĂŒĂŒp nĂ€idatakse enne argumendi nime. Lisaks, IR-parsingu lihtsustamiseks, on globaalsete entiteetide nimede ees sĂŒmbol @, ja kohalike nimede ees on sĂŒmbol % (funktsioon loetakse samuti globaalsete entiteetideks).

Üks selle koodi omadustest, millele tuleks tĂ€helepanu pöörata, on see, et Go tĂŒĂŒbi esitlemise otsus int, mis vĂ”ib olla 32-bitine vĂ”i 64-bitine, sĂ”ltuvalt kompilaatorist ja kompileerimise eesmĂ€rgist, mÀÀratakse LLVM IR koodi loomisel. See on ĂŒks paljusid pĂ”hjuseid, miks LLVM IR kood ei ole, nagu paljud arvavad, platvormist sĂ”ltumatu. Üht platvormile loodud koodi ei saa lihtsalt vĂ”tta ja teisele platvormile kompileerida (kui ei lĂ€heneta sellele probleemile eriti ettevaatlikult).

. Veel ĂŒks huvitav punkt, mida tuleks mĂ€rkida, on see, et tĂŒĂŒp i64 — ei ole allkirjastatud tĂ€isarv: see on neutraalne numbri tĂ€histamise osas. SĂ”ltuvalt kĂ€sust vĂ”ib see esindada nii allkirjastatud kui ka allkirjastamata numbreid. Liitmise operatsiooni esitlemise puhul ei oma see tĂ€htsust, seega pole siin erinevust allkirjastatud ja allkirjastamata numbrite kĂ€sitlemisel. Siinkohal tahaksin mĂ€rkida, et C keeles toob allkirjastatud tĂ€isarvuvĂ”rgu ĂŒletamine kaasa mÀÀramatut kĂ€itumist, seetĂ”ttu lisab Clangi frontend operatsioonile lipu nsw (no signed wrap), mis nĂ€itab LLVM-le, et see vĂ”ib lĂ€htuda eeldusest, et liitmisel ei toimu ĂŒletamist.

See vÔib olla oluline teatud optimeerimiste jaoks. NÀiteks kahe i16 liitmine 32-bitisel platvormil (32-bitiste registritega) nÔuab pÀrast liitmist allkirja laiendamise operatsiooni, et jÀÀda vahemikku i16. SeetÔttu on tihti tÔhusam teostada tÀisarvutehteid masina registri suurusi arvesse vÔttes.

See, mis juhtub edasi selle IR-koodiga, ei huvita meid praegu eriti. Kood optimeeritakse (aga sellise lihtsa nÀite puhul nagu meie oma ei optimeerita enam midagi) ja seejÀrel muudetakse masinkoodiks.

Teine nÀide

JÀrgmine nÀide, mida me vaatame, on pisut keerulisem. Nimelt rÀÀgime funktsioonist, mis summeerib tÀisarvude viilu:

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

See kood muudetakse jÀrgnevaks Go SSA-koodiks:

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

Siit on juba nĂ€ha rohkem konstruktsioone, mis on iseloomulikud koodi esitamiseks SSA vormis. TĂ”enĂ€oliselt on kĂ”ige silmatorkavam omadus see, et siin puuduvad struktureeritud voosĂŒsteemide juhtimise kĂ€sud. Voo juhtimiseks on siin ainult tingimuslikud ja tingimusteta ĂŒleminekud, ja kui arvestada seda kĂ€sku voojuhtimise kĂ€suna, siis on ka tagastamiskĂ€sk.

Tegelikult on tĂ€helepanuvÀÀrne, et programm ei ole jagatud plokkideks kasutades sĂŒmboolseid kommatoote (nagu C-perekonna keeltes). See on jagatud siltide abil, mis meenutab assambleekeele ja esitatakse pĂ”hiblokkidena. SSA-s nimetatakse pĂ”hiblokkideks jĂ€rjepidevaid koodijadasid, mis algavad sildist ja lĂ”pevad pĂ”hibloki lĂ”petamiseks mĂ”eldud kĂ€sklustega, nagu nĂ€iteks — return ja jump.

Veel ĂŒks huvitav detail selles koodis on kĂ€sk phi. See kĂ€sk on ĂŒsna ebatavaline ning selle mĂ”istmiseks vĂ”ib vajada aega. Pidage meeles, et SSA — on lĂŒhend Static Single Assignment. See on vahend, mida kompilaatorid kasutavad koodi esitlemiseks, kus igale muutujale antakse vÀÀrtus vaid ĂŒhe korra. See sobib suurepĂ€raselt lihtsate funktsioonide jaoks, nagu meie funktsioon myAdd, nagu eelnevalt nĂ€idatud, kuid keerukamate funktsioonide, nagu selles jaotises kĂ€sitletud funktsioonide jaoks, ei sobi see. sum. Erinevate muutuja vÀÀrtuste muutumine toimub tsĂŒkli kĂ€igus. i ja n.

SSA ĂŒletab muutuja vÀÀrtuste ĂŒhekordse mÀÀramise piiri, kasutades niinimetatud kĂ€sku phi (nime, mis on pĂ€rit kreeka tĂ€hestikust). Nimelt, et SSA-koodi esitus saaks luua keeltele nagu C, peab rakendama teatud trikke. KĂ€sk tagastab muutuja praeguse vÀÀrtuse (i vĂ”i n), ja selle parameetriteks on pĂ”hiblokkide loend. NĂ€iteks, vaatame sellist kĂ€sku:

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

Selle tĂ€hendus on jĂ€rgmine: kui eelmine pĂ”hiblokk oli blokk entry sisse, siis t0 on konstant 0, ja kui eelmine pĂ”hiblokk oli for.body, siis tuleb vĂ”tta vÀÀrtus t6 sellelt blokilt. See vĂ”ib tunduda ĂŒsna salapĂ€rane, kuid tĂ€nu sellele mehhanismile tĂ”husalt toimib SSA. Inimese vaatenurgast teeb see koodi arusaamise keerukaks, kuid see, et igale vÀÀrtusele mÀÀratakse vÀÀrtus vaid korra, lihtsustab oluliselt paljusid optimeerimisi.

Pange tÀhele, et kui te kirjutate oma kompilaatori, siis tavaliselt ei pea te selliste asjadega tegelema. I isegi Clang ei genereeri kÔiki neid kÀsklusi phi, ta kasutab mehhanismi alloca (mis meenutab tavaliste kohalike muutujate kÀsitsemist). Siis, optimeerimise kÀigu ajal, mida nimetatakse mem2reg, kÀsud alloca muudetakse SSA vormi. TinyGo, aga, saab sisendi Go SSA-lt, mis on mugav, juba konverteeritud SSA vormi.

Veel ĂŒks uudsus kĂ€sitletud vahepealse koodi fragmendis on see, et juurdepÀÀs viilu elementidele indeksi kaudu esitatakse aadressi arvutamise operaatorina ja saadud nĂ€idiku de-referentseerimise operaatorina. Siin on nĂ€ha ka konstantide otsene lisamine IR-koodi (nt — 1:int). NĂ€ites koos funktsiooniga myAdd sarnast ei ole kasutatud. NĂŒĂŒd, kui oleme nende eripĂ€radega tutvunud, vaatame, kuidas see kood muutub, kui see muudetakse LLVM IR vormi:

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
}

Siin, nagu ka varem, nĂ€eme sama struktuuri, mis sisaldab teisi sĂŒntaktilisi konstruktsioone. NĂ€iteks kutses phi on vahetatud vÀÀrtused ja sildid. Siiski on siin midagi, millele tasub eraldi tĂ€helepanu pöörata.

Esiteks, siin on nĂ€ha tĂ€iesti erinev funktsiooni allkiri. LLVM ei toeta lĂ”ike, mistĂ”ttu optimeerimise tulemusena jagas TinyGo kompilaator, mis genereeris selle vahefaasi koodi, selle andmestruktuuri kirjelduse osadeks. Ta oleks vĂ”inud esitada kolm lĂ”ike elementi (ptr, len ja cap) struktuurina (struct), kuid nende esitlemine kui kolme eraldi ŃŃƒŃ‰ĐœĐŸŃŃ‚Đž vĂ”imaldab teostada teatud optimiseerimisi. Teised kompilaatorid vĂ”ivad lĂ”ike esitada veelgi muul viisil, see sĂ”ltub sihtplatvormi funktsioonide kutsumis kokkulepetest.

Teine huvitav joon selle koodi juures on kĂ€su kasutamine getelementptr (sageli nimetatakse seda lĂŒhidalt GEP-ks).

See kÀsk töötab pointeritega ja seda kasutatakse lÔike elemendi pointeri saamiseks. NÀiteks vÔrrelgem seda jÀrgmise C keeles kirjutatud koodiga:

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

VÔi jÀrgmisega, mis on sellele ekvivalentne:

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

Peamine asi on see, et kÀsk getelementptr ei teosta dereferentseerimise toiminguid. Ta ainult arvutab uue pointeri, pÔhinedes olemasoleval. Seda saab kÀsitleda nagu kÀsku mul ja add riistvaratasandil. Lisainfot GEP kÀsu kohta saab lugeda siin.

Veel ĂŒks huvitav joon selle vahefaasi koodi juures on kĂ€su kasutamine icmp. See on juhend, mida kasutatakse tĂ€isarvude vĂ”rdlemiseks. Selle juhendi tĂ€itmise tulemuseks on alati vÀÀrtus tĂŒĂŒbiga i1 — loogiline vÀÀrtus. Antud juhul toimub vĂ”rdlemine, kasutades mĂ€rksĂ”na slt (signaal vĂ€hem kui), kuna me vĂ”rdleme kahte numbrit, mis on varem esitatud tĂŒĂŒbina int. Kui me vĂ”rdleksime kahte mitteallkirjastatud tĂ€isarvu, siis kasutaksime juhendina icmp, samas kui vĂ”rdlemisel kasutatav mĂ€rksĂ”na oleks ult. ÜlejÀÀnud ujuvkomaga numbrite vĂ”rdlemiseks kasutatakse teistsugust juhendit, fcmp, mis toimib sarnaselt.

Summary

Usun, et selles materjalis olen kĂ€sitlenud kĂ”ige olulisemaid LLVM IR-i omadusi. Loomulikult on veel palju muud. EelkĂ”ige vĂ”ivad vahekoodi esitluses olla mitmed annotatsioonid, mis vĂ”imaldavad optimeerimise lĂ€bimisel arvesse vĂ”tta teatud koodi omadusi, mis on kompilaatorile tuntud ja mida ei saa teisel viisil IR-is vĂ€ljendada. NĂ€iteks on see mĂ€rk inbounds GEP-i juhendi puhul, vĂ”i mĂ€rgid nsw ja nuw, mida vĂ”ib lisada juhendile add. Sama kehtib ka mĂ€rksĂ”na kohta private, mis nĂ€itab optimeerijale, et seda tĂ€histatud funktsiooni ei kutsuta vĂ€lja praegusest kompileerimise ĂŒksusest. See vĂ”imaldab lĂ€bi viia palju huvitavaid vahelisi optimeerimisi, nĂ€iteks mittekasutatavate argumentide kĂ”rvaldamist.

Teavet LLVM-i kohta saate lugeda dokumentatsioon, millega te tihti kokku puutute, kui arendate oma LLVM-pÔhist kompilaatorit. Siin on juhend, mis kÀsitleb kompilaatori arendamist vÀga lihtsa keele jaoks. Need kaks allikat on teile kasulikud oma kompilaatori loomisel.

Lugupeetud lugejad! Kas kasutate LLVM-i?

LLVM Go vaatenurgast

Allikas: habr.com

Osta usaldusvÀÀrne hostimine veebilehtede jaoks DDoS-i kaitsega, VPS VDS serverid đŸ”„ Osta usaldusvÀÀrne hostimine veebilehtede jaoks DDoS-i kaitsega, VPS VDS serverid | ProHoster