LLVM frá Go sjónarhorni

Það er mjög erfitt verkefni að þróa þýðanda. En sem betur fer, með þróun verkefna eins og LLVM, er lausnin á þessu vandamáli einfölduð til muna, sem gerir jafnvel einum forritara kleift að búa til nýtt tungumál sem er nálægt C í frammistöðu. Vinna með LLVM er flókið af því að þetta kerfið er táknað með miklu magni af kóða, búið litlum skjölum. Til að reyna að leiðrétta þennan annmarka ætlar höfundur efnisins, sem við birtum í dag þýðinguna á, sýna dæmi um kóða sem skrifaður er í Go og sýna hvernig hann er fyrst þýddur á Áfram SSA, og síðan í LLVM IR með því að nota þýðandann tinyGO. Go SSA og LLVM IR kóðanum hefur verið breytt lítillega til að fjarlægja hluti sem eiga ekki við útskýringarnar sem gefnar eru hér, til að gera skýringarnar skiljanlegri.

LLVM frá Go sjónarhorni

Fyrsta dæmið

Fyrsta aðgerðin sem ég ætla að skoða hér er einfalt kerfi til að bæta við tölum:

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

Þessi aðgerð er mjög einföld og ef til vill gæti ekkert verið einfaldara. Það þýðir í eftirfarandi Go SSA kóða:

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

Með þessu útsýni eru vísbendingar um gagnategund settar til hægri og hægt er að hunsa þær í flestum tilfellum.

Þetta litla dæmi gerir þér nú þegar kleift að sjá kjarnann í einum þætti SSA. Þegar kóða er breytt í SSA form er hver tjáning nefnilega sundurliðuð í grunnhlutana sem hún er samsett úr. Í okkar tilviki, skipunin return a + b, í raun táknar tvær aðgerðir: að bæta við tveimur tölum og skila niðurstöðunni.

Að auki, hér geturðu séð grunnblokkir forritsins; í þessum kóða er aðeins einn blokk - inngangsreiturinn. Við munum tala meira um blokkir hér að neðan.

Go SSA kóðanum breytist auðveldlega í LLVM IR:

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

Það sem þú getur tekið eftir er að þó mismunandi setningafræðileg uppbygging sé notuð hér, þá er uppbygging fallsins í grundvallaratriðum óbreytt. LLVM IR kóðinn er aðeins sterkari en Go SSA kóðinn, svipaður og C. Hér, í fallyfirlýsingunni, er fyrst lýsing á gagnategundinni sem hún skilar, tegund breytu er tilgreind á undan nafni breytu. Að auki, til að einfalda IR þáttun, er táknið á undan nöfnum alþjóðlegra aðila @, og á undan staðbundnum nöfnum er tákn % (fall er einnig talið alþjóðleg eining).

Eitt sem þarf að hafa í huga varðandi þennan kóða er að tegundarákvörðun Go int, sem hægt er að tákna sem 32-bita eða 64-bita gildi, allt eftir þýðanda og markmiði söfnunarinnar, er samþykkt þegar LLVM býr til IR kóðann. Þetta er ein af mörgum ástæðum þess að LLVM IR kóði er ekki, eins og margir halda, vettvangsóháður. Slíkan kóða, búinn til fyrir einn vettvang, er ekki einfaldlega hægt að taka og setja saman fyrir annan vettvang (nema þú hentir til að leysa þetta vandamál með mikilli aðgát).

Annar áhugaverður punktur sem vert er að taka fram er að tegundin i64 er ekki heiltala með formerkjum: hún er hlutlaus hvað varðar tákn tölunnar. Það fer eftir leiðbeiningunum, það getur táknað bæði áritað og óundirritað númer. Þegar um er að ræða framsetningu samlagningaraðgerðarinnar skiptir þetta ekki máli, þannig að það er enginn munur á því að vinna með táknaðar eða ómerktar tölur. Hér vil ég taka fram að á C tungumálinu leiðir yfirfylling á heiltölubreytu með tákni til óskilgreindrar hegðunar, þannig að Clang framhliðin bætir fána við aðgerðina nsw (engin undirrituð umbúðir), sem segir LLVM að það megi gera ráð fyrir að viðbótin flæði aldrei yfir.

Þetta gæti verið mikilvægt fyrir sumar hagræðingar. Til dæmis að bæta við tveimur gildum i16 á 32-bita vettvangi (með 32-bita skrám) krefst þess, eftir að hafa verið bætt við, stækkunaraðgerð til að haldast innan sviðs i16. Vegna þessa er oft skilvirkara að framkvæma heiltöluaðgerðir byggðar á stærðum vélaskrár.

Hvað gerist næst með þennan IR kóða er ekki sérstakt áhugamál fyrir okkur núna. Kóðinn er fínstilltur (en ef um er að ræða einfalt dæmi eins og okkar er ekkert fínstillt) og síðan breytt í vélkóða.

Annað dæmi

Næsta dæmi sem við skoðum verður aðeins flóknara. Við erum nefnilega að tala um fall sem leggur saman sneið af heiltölum:

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

Þessi kóði breytist í eftirfarandi Go SSA kóða:

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

Hér geturðu nú þegar séð fleiri byggingar sem eru dæmigerðar til að tákna kóða á SSA formi. Kannski er augljósasti eiginleiki þessa kóða sú staðreynd að það eru engar skipulagðar flæðistýringarskipanir. Til að stjórna flæði útreikninga eru aðeins skilyrt og skilyrðislaus stökk, og ef við lítum á þessa skipun sem skipun til að stjórna flæðinu, afturskipun.

Reyndar er hægt að fylgjast með því að forritinu er ekki skipt í kubba með því að nota krullaðar axlabönd (eins og í C fjölskyldu tungumálanna). Það er skipt með merkimiðum, sem minnir á samsetningarmál, og sett fram í formi grunnkubba. Í SSA eru grunnblokkir skilgreindir sem samfelldar kóðaraðir sem byrja á merkimiða og endar á grunnleiðbeiningum um útfyllingu blokka, eins og - return и jump.

Annað áhugavert smáatriði þessa kóða er táknað með leiðbeiningunum phi. Leiðbeiningarnar eru frekar óvenjulegar og geta tekið nokkurn tíma að skilja þær. mundu það S.S.A. er stytting á Static Single Assignment. Þetta er milliframsetning á kóðanum sem þýðendur nota, þar sem hverri breytu er aðeins úthlutað einu sinni gildi. Þetta er frábært til að tjá einfaldar aðgerðir eins og aðgerðina okkar myAddsýnt hér að ofan, en hentar ekki fyrir flóknari aðgerðir eins og fallið sem fjallað er um í þessum kafla sum. Sérstaklega breytast breytur við framkvæmd lykkjunnar i и n.

SSA framhjá takmörkunum á að úthluta breytugildum einu sinni með því að nota svokallaða leiðbeiningar phi (nafn þess er tekið úr gríska stafrófinu). Staðreyndin er sú að til þess að hægt sé að búa til SSA framsetningu kóða fyrir tungumál eins og C, verður þú að grípa til nokkurra brellna. Niðurstaðan af því að kalla þessa leiðbeiningu er núverandi gildi breytunnar (i eða n), og listi yfir grunnblokkir er notaður sem færibreytur þess. Íhugaðu til dæmis þessa leiðbeiningar:

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

Merking þess er sem hér segir: ef fyrri grunnblokkin var blokk entry (inntak), þá t0 er fasti 0, og ef fyrri grunnblokkin var for.body, þá þarftu að taka gildið t6 úr þessari blokk. Þetta kann allt að virðast frekar dularfullt, en þetta kerfi er það sem gerir SSA að virka. Frá mannlegu sjónarhorni gerir þetta allt kóðann erfitt að skilja, en sú staðreynd að hverju gildi er úthlutað einu sinni gerir margar hagræðingar mun auðveldari.

Athugaðu að ef þú skrifar þinn eigin þýðanda þarftu venjulega ekki að takast á við svona dót. Jafnvel Clang býr ekki til allar þessar leiðbeiningar phi, það notar vélbúnað alloca (það líkist því að vinna með venjulegar staðbundnar breytur). Síðan, þegar keyrt er LLVM hagræðingarpassi kallaður mem2reg, leiðbeiningar alloca breytt í SSA form. TinyGo fær hins vegar inntak frá Go SSA, sem, þægilega, er nú þegar breytt í SSA form.

Önnur nýjung á broti millikóða sem er til skoðunar er að aðgangur að sneiðþáttum eftir vísitölu er sýndur í formi aðgerð til að reikna út heimilisfangið og aðgerð til að vísa frá bendilinn sem myndast. Hér getur þú séð beina samlagningu fasta við IR kóðann (til dæmis - 1:int). Í dæminu með fallinu myAdd þetta hefur ekki verið notað. Nú þegar við höfum komist þessa eiginleika úr vegi skulum við skoða hvað þessi kóði verður þegar hann er breytt í LLVM IR form:

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
}

Hér, eins og áður, getum við séð sömu uppbyggingu, sem inniheldur aðrar setningafræðilegar byggingar. Til dæmis í símtölum phi gildum og merkjum skipt út. Hins vegar er hér eitthvað sem vert er að gefa sérstakan gaum.

Til að byrja með, hér geturðu séð allt aðra virka undirskrift. LLVM styður ekki sneiðar og þar af leiðandi, sem hagræðing, skipti TinyGo þýðandinn sem bjó til þennan millikóða lýsingu á þessari gagnauppbyggingu í hluta. Það gæti táknað þrjú sneiðarefni (ptr, len и cap) sem uppbyggingu (struct), en að tákna þá sem þrjár aðskildar einingar gerir ráð fyrir nokkrum hagræðingum. Aðrir þýðendur geta táknað sneiðina á annan hátt, allt eftir kallavenjum aðgerða markvettvangsins.

Annar áhugaverður eiginleiki þessa kóða er notkun leiðbeininganna getelementptr (oft skammstafað sem GEP).

Þessi kennsla vinnur með ábendingum og er notuð til að fá bendil á sneiðeiningu. Til dæmis, við skulum bera það saman við eftirfarandi kóða skrifaðan í C:

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

Eða með eftirfarandi jafngildi þessu:

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

Það mikilvægasta hér er að leiðbeiningarnar getelementptr framkvæmir ekki frávísunaraðgerðir. Það reiknar bara nýjan bendi út frá þeim sem fyrir er. Það má taka það sem leiðbeiningar mul и add á vélbúnaðarstigi. Þú getur lesið meira um GEP leiðbeiningarnar hér.

Annar áhugaverður eiginleiki þessa millikóða er notkun leiðbeininganna icmp. Þetta er almenn leiðbeining sem notuð er til að útfæra heiltölusamanburð. Niðurstaða þessarar kennslu er alltaf tegundargildi i1 — rökrétt gildi. Í þessu tilviki er samanburður gerður með því að nota leitarorðið slt (undirritað minna en), þar sem við erum að bera saman tvær tölur sem áður voru táknaðar með gerðinni int. Ef við værum að bera saman tvær ómerktar heiltölur, þá myndum við nota icmp, og leitarorðið sem notað er í samanburðinum væri ult. Til að bera saman flottölur er önnur leiðbeining notuð, fcmp, sem virkar á svipaðan hátt.

Niðurstöður

Ég tel að í þessu efni hafi ég farið yfir mikilvægustu eiginleika LLVM IR. Hér er auðvitað margt fleira. Sérstaklega getur milliframsetning kóðans innihaldið margar athugasemdir sem leyfa hagræðingarleiðum til að taka tillit til ákveðinna eiginleika kóðans sem þýðandinn þekkir og ekki er hægt að tjá með öðrum hætti í IR. Til dæmis er þetta fáni inbounds GEP leiðbeiningar, eða fánar nsw и nuw, sem hægt er að bæta við leiðbeiningarnar add. Sama gildir um leitarorðið private, sem gefur til kynna fyrir fínstillingu að ekki sé vísað til aðgerðarinnar sem það merkir utan núverandi safneiningar. Þetta gerir ráð fyrir mörgum áhugaverðum hagræðingum á milli aðferða eins og að útrýma ónotuðum rökum.

Þú getur lesið meira um LLVM í skjöl, sem þú vísar oft til þegar þú þróar þinn eigin LLVM-byggða þýðanda. Hérna forystu, sem lítur á þróun þýðanda fyrir mjög einfalt tungumál. Báðar þessar heimildir munu nýtast þér þegar þú býrð til þinn eigin þýðanda.

Kæru lesendur! Ertu að nota LLVM?

LLVM frá Go sjónarhorni

Heimild: www.habr.com

Bæta við athugasemd