LLVM Go ikuspegitik

Konpiladore bat garatzea oso lan zaila da. Baina, zorionez, LLVM bezalako proiektuen garapenarekin, arazo honen konponbidea asko errazten da, eta horri esker, programatzaile bakar batek ere C-ren errendimendutik hurbil dagoen lengoaia berri bat sortzea ahalbidetzen du. LLVMrekin lan egitea zaildu egiten da hau. sistema kode kopuru handi batek irudikatzen du, dokumentazio gutxiz hornitua. Gabezia hori zuzentzen saiatzeko, materialaren egileak, zeinaren itzulpena gaur argitaratzen dugun, Go-n idatzitako kodeen adibideak erakutsiko ditu eta lehen aldiz nola itzultzen diren erakutsiko du. Joan SSA, eta gero LLVM IR-n konpilatzailea erabiliz tinyGO. Go SSA eta LLVM IR kodea apur bat editatu da hemen emandako azalpenetarako garrantzitsuak ez diren gauzak kentzeko, azalpenak ulergarriagoak izan daitezen.

LLVM Go ikuspegitik

Lehenengo adibidea

Hemen aztertuko dudan lehenengo funtzioa zenbakiak gehitzeko mekanismo sinple bat da:

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

Funtzio hau oso sinplea da, eta, beharbada, ezer sinpleagoa izango da. Go SSA kode honetan itzultzen da:

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

Ikuspegi honekin, funtzioaren datu-moten aholkuak eskuinaldean jartzen dira eta kasu gehienetan ez ikusi egin daitezke.

Adibide txiki honek dagoeneko SSAren alderdi baten funtsa ikusteko aukera ematen du. Hots, kodea SSA formara bihurtzean, esamolde bakoitza osatzen den zatirik oinarrizkoenetan banatzen da. Gure kasuan, komandoa return a + b, izan ere, bi eragiketa adierazten ditu: bi zenbaki batu eta emaitza itzultzea.

Horrez gain, hemen programaren oinarrizko blokeak ikus ditzakezu; kode honetan bloke bakarra dago: sarrera blokea. Jarraian blokeei buruz gehiago hitz egingo dugu.

Go SSA kodea erraz bihurtzen da LLVM IR-ra:

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

Ikus dezakezuna da hemen egitura sintaktiko desberdinak erabiltzen diren arren, funtzioaren egitura funtsean ez dela aldatu. LLVM IR kodea Go SSA kodea baino apur bat indartsuagoa da, C-ren antzekoa. Hemen, funtzio-adierazpenean, lehenik itzultzen duen datu-motaren deskribapena dago, argumentu mota argumentuaren izenaren aurretik adierazten da. Horrez gain, IR analisia sinplifikatzeko, entitate globalen izenak sinboloa jartzen dute aurretik @, eta tokiko izenen aurretik ikur bat dago % (funtzio bat entitate globaltzat ere hartzen da).

Kode honi buruz ohartu beharreko gauza bat da Go-ren motako irudikapen erabakia int, 32 biteko edo 64 biteko balio gisa irudika daitekeena, konpilatzailearen eta konpilazioaren xedearen arabera, LLVM-k IR kodea sortzen duenean onartzen da. Hau da LLVM IR kodea, jende askok uste duen bezala, plataforma independentea ez izatearen arrazoi askotako bat. Kode hori, plataforma baterako sortua, ezin da beste plataforma baterako hartu eta konpilatu (arazo hau konpontzeko egokia ez bazara behintzat. arreta handiz).

Aipatzeko moduko beste puntu interesgarri bat mota hori da i64 ez da zenbaki oso sinduna: neutroa da zenbakiaren zeinua adierazteari dagokionez. Argibidearen arabera, zenbaki sinatuak zein sinatu gabekoak irudika ditzake. Batuketa eragiketaren irudikapenaren kasuan, horrek ez du axola, beraz, ez dago desberdintasunik sinatu edo sinatu gabeko zenbakiekin lan egiteko. Hemen adierazi nahi nuke C hizkuntzan, sinatutako osoko aldagai bat gainezka egiteak portaera definitu gabe eragiten duela, beraz, Clang frontend-ak bandera bat gehitzen dio eragiketari. nsw (ez dago sinatutako wrap), LLVM-ri esaten dion gehikuntzak inoiz gainezka egiten duela suposa dezakeela.

Hau garrantzitsua izan daiteke optimizazio batzuetarako. Adibidez, bi balio gehitzea i16 32 biteko plataforma batean (32 biteko erregistroekin) seinaleak zabaltzeko eragiketa bat behar du, gainera, barrutian jarraitzeko. i16. Horregatik, sarritan eraginkorragoa da makinen erregistroen tamainetan oinarritutako osoko eragiketak egitea.

IR kode honekin gero gertatzen dena ez zaigu bereziki interesgarri orain. Kodea optimizatzen da (baina gurea bezalako adibide soil baten kasuan, ez da ezer optimizatzen) eta gero makina-kode bihurtzen da.

Bigarren adibidea

Aztertuko dugun hurrengo adibidea pixka bat konplexuagoa izango da. Hots, zenbaki osoen zati bat batzen duen funtzio bati buruz ari gara:

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

Kode hau Go SSA kode honetara bihurtzen da:

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

Hemen dagoeneko ikus ditzakezu SSA formularioan kodea irudikatzeko ohiko eraikuntza gehiago. Beharbada, kode honen ezaugarririk nabarmenena fluxua kontrolatzeko komando egituraturik ez egotea da. Kalkuluen fluxua kontrolatzeko, baldintzapeko eta baldintzarik gabeko jauziak baino ez daude, eta, komando hau fluxua kontrolatzeko komandotzat hartzen badugu, itzulerako agindua.

Izan ere, hemen arreta jarri ahal izango duzu programa ez dagoela blokeetan banatuta giltza kizkurren bidez (C hizkuntzaren familian bezala). Etiketen bidez banatuta dago, muntaia-lengoaiak gogorarazten dituena, eta oinarrizko blokeen moduan aurkezten da. SSAn, oinarrizko blokeak etiketa batekin hasi eta oinarrizko blokeak osatzeko jarraibideekin amaitzen diren kode-sekuentzia ondoko sekuentzia gisa definitzen dira, hala nola - return ΠΈ jump.

Kode honen beste xehetasun interesgarri bat instrukzioak adierazten du phi. Argibideak nahiko ezohikoak dira eta denbora pixka bat behar da ulertzeko. gogoratu SSA Laburra da Estatic Single Assignment. Hau konpilatzaileek erabiltzen duten kodearen tarteko irudikapena da, non aldagai bakoitzari behin bakarrik balio bat esleitzen zaion. Hau oso ona da gure funtzioa bezalako funtzio sinpleak adierazteko myAddgoian agertzen dena, baina ez da egokia atal honetan aztertutako funtziorako funtzio konplexuagoetarako sum. Bereziki, aldagaiak aldatzen dira begiztaren exekuzioan i ΠΈ n.

SSAk balio aldagaiak esleitzeko murrizketa saihesten du behin instrukzioa deiturikoak erabiliz phi (Bere izena alfabeto grekotik hartua da). Kontua da SSA kodearen irudikapena C bezalako hizkuntzetarako sortu ahal izateko, trikimailu batzuetara jo behar duzula. Instrukzio honi deitzearen emaitza aldagaiaren uneko balioa da (i edo n), eta oinarrizko blokeen zerrenda erabiltzen da bere parametro gisa. Adibidez, kontuan hartu argibide hau:

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

Bere esanahia honako hau da: aurreko oinarrizko blokea bloke bat balitz entry (sarrera), orduan t0 konstante bat da 0, eta aurreko oinarrizko blokea bazen for.body, orduan hartu behar duzu balioa t6 bloke honetatik. Horrek guztiak nahiko misteriotsua dirudi, baina mekanismo honek SSA funtzionatzen duena da. Gizakiaren ikuspegitik, horrek guztiak zaila egiten du kodea ulertzea, baina balio bakoitza behin bakarrik esleituta egoteak optimizazio asko errazten ditu.

Kontuan izan zure konpilatzailea idazten baduzu, normalean ez duzula mota honetako gauzei aurre egin beharko. Clang-ek ere ez ditu argibide hauek guztiak sortzen phi, mekanismo bat erabiltzen du alloca (aldagai lokal arruntekin lan egitearen antza du). Ondoren, LLVM optimizazio-pase bat exekutatzen denean mem2reg, argibideak alloca SSA formara bihurtu da. TinyGo-k, ordea, Go SSAren sarrera jasotzen du, eta hori, komeni den, dagoeneko SSA formara bihurtu da.

Kontuan hartutako tarteko kode zatiaren beste berrikuntza bat da indizearen bidez zatikako elementuetarako sarbidea helbidea kalkulatzeko eragiketa eta ondoriozko erakuslea deserreferentziatzeko eragiketa baten moduan adierazten dela. Hemen IR kodeari konstanteen gehiketa zuzena ikus dezakezu (adibidez - 1:int). Funtzioa duen adibidean myAdd hau ez da erabili. Ezaugarri horiek alde batera utzita, ikus dezagun zer bihurtzen den kode hau LLVM IR formara bihurtzen denean:

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
}

Hemen, lehen bezala, egitura bera ikus dezakegu, beste egitura sintaktiko batzuk barne hartzen dituena. Adibidez, deietan phi balioak eta etiketak trukatu. Hala ere, bada hemen arreta berezia jartzea merezi duen zerbait.

Hasteko, hemen funtzio sinadura guztiz desberdina ikus dezakezu. LLVM-k ez ditu xerrak onartzen, eta, ondorioz, optimizazio gisa, tarteko kode hau sortu duen TinyGo konpilatzaileak datu-egitura honen deskribapena zatitan zatitu du. Hiru xerra elementu irudika ditzake (ptr, len ΠΈ cap) egitura gisa (egitura), baina hiru entitate bereizi gisa irudikatzeak optimizazio batzuk ahalbidetzen ditu. Beste konpilatzaile batzuek xerra beste modu batzuetan irudika dezakete, xede-plataformaren funtzioen dei-konbentzioen arabera.

Kode honen beste ezaugarri interesgarri bat instrukzioaren erabilera da getelementptr (askotan GEP gisa laburtua).

Instrukzio honek erakusleekin funtzionatzen du eta zatikako elementu baten erakuslea lortzeko erabiltzen da. Adibidez, aldera dezagun C-n idatzitako kode honekin:

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

Edo honen baliokide honekin:

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

Hemen garrantzitsuena argibideak da getelementptr ez du deserreferentziatze eragiketarik egiten. Erakusle berri bat kalkulatzen du lehendik dagoenaren arabera. Argibide gisa har daiteke mul ΠΈ add hardware mailan. GEP argibideei buruz gehiago irakur dezakezu Hemen.

Tarteko kode honen beste ezaugarri interesgarri bat instrukzioaren erabilera da icmp. Zenbaki osoen konparaketak ezartzeko erabiltzen den helburu orokorreko instrukzioa da. Instrukzio honen emaitza motaren balio bat da beti i1 - balio logikoa. Kasu honetan, konparaketa bat egiten da gako-hitza erabiliz slt (baino gutxiago sinatuta), aurretik motaren bidez irudikatutako bi zenbaki konparatzen ari baikara int. Zeinu gabeko bi zenbaki oso alderatuko bagenitu, orduan erabiliko genuke icmp, eta konparazioan erabilitako gakoa izango litzateke ult. Koma mugikorreko zenbakiak alderatzeko, beste instrukzio bat erabiltzen da, fcmp, antzera funtzionatzen duena.

Emaitzak

Uste dut material honetan LLVM IRren ezaugarri garrantzitsuenak estali ditudala. Jakina, hemen askoz gehiago dago. Bereziki, kodearen tarteko irudikapenak oharpen ugari izan ditzake optimizazio-pasei esker, konpilatzaileak ezagutzen dituen kodearen zenbait ezaugarri kontuan izan ditzaten, bestela IRn adierazi ezin direnak. Adibidez, hau bandera bat da inbounds GEP argibideak edo banderak nsw ΠΈ nuw, argibideetara gehi daitekeena add. Gauza bera gertatzen da gako-hitzarekin private, optimizatzaileari adieraziz markatzen duen funtzioa ez dela erreferentziarik izango uneko konpilazio-unitatetik kanpo. Horrek prozeduren arteko optimizazio interesgarri asko ahalbidetzen ditu, adibidez, erabiltzen ez diren argumentuak ezabatzea.

LLVM-i buruz gehiago irakur dezakezu dokumentazioa, maiz aipatuko duzun zure LLVM-n oinarritutako konpilatzailea garatzen duzunean. Hemen lidergoa, oso hizkuntza sinple baterako konpilatzailea garatzea aztertzen duena. Bi informazio-iturri hauek erabilgarriak izango zaizkizu zure konpilatzailea sortzerakoan.

Irakurle maitea! LLVM erabiltzen ari al zara?

LLVM Go ikuspegitik

Iturria: www.habr.com

Gehitu iruzkin berria