LLVM aus enger Go Perspektiv

E Compiler z'entwéckelen ass eng ganz schwéier Aufgab. Mä, glécklecherweis, mat der Entwécklung vu Projeten wéi LLVM, ass d'Léisung fir dëse Problem immens vereinfacht, wat souguer engem eenzege Programméierer erlaabt eng nei Sprooch ze kreéieren, déi an der Leeschtung no C ass. System gëtt duerch eng enorm Quantitéit vu Code vertruede, mat wéineg Dokumentatioun ausgestatt. Fir ze probéieren dës Defizit ze korrigéieren, wäert den Auteur vum Material, d'Iwwersetzung vun deem mir haut verëffentlechen, Beispiller vu Code weisen, déi am Go geschriwwe sinn a weisen wéi se fir d'éischt an iwwersat ginn. Gitt SSA, an dann am LLVM IR mam Compiler benotzt tinyGO. De Go SSA an LLVM IR Code gouf liicht geännert fir Saachen ze läschen déi net relevant sinn fir d'Erklärungen hei ginn, fir d'Erklärungen méi verständlech ze maachen.

LLVM aus enger Go Perspektiv

Éischt Beispill

Déi éischt Funktioun, déi ech hei kucken, ass en einfache Mechanismus fir Zuelen ze addéieren:

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

Dës Funktioun ass ganz einfach, a vläicht näischt kéint méi einfach sinn. Et iwwersetzt an de folgende Go SSA Code:

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

Mat dëser Vue sinn d'Datentyp Hiweiser op der rietser Säit plazéiert a kënnen an de meeschte Fäll ignoréiert ginn.

Dëst klengt Beispill erlaabt Iech schonn d'Essenz vun engem Aspekt vun SSA ze gesinn. Nämlech, wann Dir Code an SSA Form konvertéiert, gëtt all Ausdrock an déi elementarst Deeler opgedeelt, aus deenen et besteet. An eisem Fall, de Kommando return a + b, Tatsächlech, stellt zwou Operatiounen: zwou Zuelen dobäi an d'Resultat zréck.

Zousätzlech, hei kënnt Dir d'Basisblocken vum Programm gesinn; an dësem Code gëtt et nëmmen ee Block - den Entréesblock. Mir schwätze méi iwwer Blocks hei drënner.

De Go SSA Code konvertéiert einfach op LLVM IR:

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

Wat Dir bemierkt ass datt obwuel verschidde syntaktesch Strukture hei benotzt ginn, ass d'Struktur vun der Funktioun am Fong onverännert. De LLVM IR Code ass e bësse méi staark wéi de Go SSA Code, ähnlech wéi C. Hei, an der Funktiounserklärung, gëtt et als éischt eng Beschreiwung vun der Datetyp déi se zréckginn, den Argumenttyp gëtt virum Argumentnumm uginn. Zousätzlech, fir d'IR-Parsing ze vereinfachen, ginn d'Nimm vun de globalen Entitéite vum Symbol virausgesat. @, a virun lokal Nimm gëtt et e Symbol % (eng Funktioun gëtt och als global Entitéit ugesinn).

Eng Saach iwwer dëse Code ze notéieren ass datt Go's Typ Representatiounsentscheedung int, déi als 32-Bit oder 64-Bit Wäert vertruede ka ginn, jee no dem Compiler an dem Zil vun der Kompilatioun, gëtt ugeholl wann LLVM den IR Code generéiert. Dëst ass ee vun de ville Grënn datt LLVM IR Code net ass, wéi vill Leit mengen, Plattform onofhängeg. Esou Code, erstallt fir eng Plattform, kann net einfach geholl a fir eng aner Plattform zesummegesat ginn (ausser Dir sidd gëeegent fir dëse Problem ze léisen mat extremer Suergfalt).

En aneren interessante Punkt, deen opmierksam ass, ass datt d'Typ i64 ass net en ënnerschriwwen ganz Zuel: et ass neutral wat d'Zeeche vun der Zuel representéiert. Ofhängeg vun der Instruktioun, kann et souwuel ënnerschriwwen an net ënnerschriwwen Zuelen representéieren. Am Fall vun der Representatioun vun der Zousatzoperatioun ass dëst egal, sou datt et keen Ënnerscheed ass fir mat ënnerschriwwenen oder net ënnerschriwwenen Zuelen ze schaffen. Hei wëll ech bemierken datt an der C Sprooch iwwerflësseg vun enger ënnerschriwwener ganzer Variabel zu ondefinéiert Verhalen féiert, sou datt de Clang Frontend e Fändel un d'Operatioun bäidréit nsw (keng ënnerschriwwen Wrap), déi seet LLVM datt et dovun ausgoen kann, datt Zousatz ni iwwerflësseg.

Dëst kann wichteg sinn fir e puer Optimisatiounen. Zum Beispill zwee Wäerter derbäi i16 op enger 32-Bit Plattform (mat 32-Bit Registere) erfuerdert, no Zousatz, eng Schëld Expansiounsoperatioun fir am Beräich ze bleiwen i16. Dofir ass et dacks méi effizient ganz Zuelen Operatiounen auszeféieren baséiert op Maschinnregistergréissten.

Wat duerno geschitt mat dësem IR Code ass net besonnesch interessant fir eis elo. De Code gëtt optimiséiert (awer am Fall vun engem einfache Beispill wéi eis gëtt näischt optimiséiert) an dann an Maschinncode ëmgewandelt.

Zweet Beispill

Dat nächst Beispill, dat mir kucken, wäert e bësse méi komplizéiert sinn. Mir schwätzen nämlech vun enger Funktioun déi e Slice vun ganz Zuelen summéiert:

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

Dëse Code konvertéiert op de folgende Go SSA Code:

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

Hei gesitt Dir scho méi Konstruktiounen typesch fir Code an der SSA Form ze representéieren. Vläicht ass déi offensichtlechst Feature vun dësem Code d'Tatsaach datt et keng strukturéiert Flowkontrollbefehle gëtt. Fir de Flux vu Berechnungen ze kontrolléieren, ginn et nëmmen bedingungslos an bedingungslos Spréng, a wa mir dëst Kommando als Kommando betruechten fir de Flux ze kontrolléieren, e Retour Kommando.

Tatsächlech, hei kënnt Dir oppassen op d'Tatsaach datt de Programm net a Blocken opgedeelt ass mat Curly Klameren (wéi an der C Famill vu Sproochen). Et ass opgedeelt duerch Etiketten, erënnert un d'Versammlungssproochen, a presentéiert a Form vu Basisblocken. An SSA sinn Basisblocken definéiert als kontinuéierlech Codesequenzen, déi mat engem Label ufänken a mat Basisblock-Fäerdegstellungsinstruktiounen ophalen, sou wéi - return и jump.

En aneren interessanten Detail vun dësem Code gëtt duerch d'Instruktioun vertruede phi. D'Instruktioune sinn zimlech ongewéinlech a kënnen e bëssen Zäit huelen fir ze verstoen. erënneren, datt S.S.A. ass kuerz fir Static Single Assignment. Dëst ass eng Zwëschenvertriedung vum Code dee vu Compilere benotzt gëtt, an deem all Variabel nëmmen eemol e Wäert zougewisen gëtt. Dëst ass super fir einfach Funktiounen auszedrécken wéi eis Funktioun myAdduewen gewisen, awer ass net gëeegent fir méi komplex Funktiounen wéi d'Funktioun an dëser Rubrik diskutéiert sum. Besonnesch Variablen änneren während der Ausféierung vun der Loop i и n.

SSA ëmgeet d'Restriktioun fir variabel Wäerter eemol mat enger sougenannter Instruktioun ze ginn phi (säin Numm ass aus dem griichesche Alphabet geholl). D'Tatsaach ass datt fir datt d'SSA Representatioun vum Code fir Sprooche wéi C generéiert gëtt, musst Dir op e puer Tricken zréckgräifen. D'Resultat vum Opruff vun dëser Instruktioun ass den aktuelle Wäert vun der Variabel (i oder n), an eng Lëscht vu Basisblocken gëtt als seng Parameter benotzt. Zum Beispill, betruecht dës Instruktioun:

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

Seng Bedeitung ass wéi follegt: wann de fréiere Basisblock e Block war entry (Input), dann t0 ass eng konstant 0, a wann de fréiere Basisblock war for.body, da musst Dir de Wäert huelen t6 vun dësem Block. Dëst kann alles ganz mysteriéis schéngen, awer dëse Mechanismus ass wat d'SSA mécht. Aus enger mënschlecher Perspektiv mécht dëst alles de Code schwéier ze verstoen, awer d'Tatsaach datt all Wäert nëmmen eemol zougewisen gëtt mécht vill Optimisatiounen vill méi einfach.

Notéiert datt wann Dir Ären eegene Compiler schreift, musst Dir normalerweis net mat dëser Aart vu Saachen ëmgoen. Och Clang generéiert net all dës Instruktiounen phi, et benotzt e Mechanismus alloca (et gläicht d'Aarbecht mat gewéinleche lokalen Variablen). Dann, wann Dir en LLVM Optimisatiounspass leeft genannt mem2 reg, Uweisungen alloca an SSA Form ëmgerechent. TinyGo kritt awer Input vu Go SSA, déi bequem schonn an SSA Form ëmgewandelt ass.

Eng aner Innovatioun vum Fragment vum Zwëschencode, deen berécksiichtegt ass, ass datt den Zougang zu Slice Elementer duerch Index a Form vun enger Operatioun vun der Berechnung vun der Adress an enger Operatioun fir de resultéierende Pointer ofzeschléissen. Hei kënnt Dir den direkten Zousatz vun Konstanten zum IR Code gesinn (zum Beispill - 1:int). Am Beispill mat der Funktioun myAdd dëst ass net benotzt ginn. Elo datt mir dës Features aus dem Wee hunn, kucke mer wat dëse Code gëtt wann se an LLVM IR Form ëmgewandelt ginn:

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
}

Hei, wéi virdrun, kënne mir déi selwecht Struktur gesinn, déi aner syntaktesch Strukturen enthält. Zum Beispill, an Uriff phi Wäerter an Etiketten ausgetauscht. Wéi och ëmmer, et gëtt eppes hei op wat et wäert ass besonnesch Opmierksamkeet ze bezuelen.

Fir unzefänken, hei kënnt Dir eng komplett aner Funktioun Ënnerschrëft gesinn. LLVM ënnerstëtzt keng Scheiwen, an als Resultat, als Optimiséierung, huet den TinyGo Compiler deen dësen Zwëschecode generéiert huet d'Beschreiwung vun dëser Datestruktur an Deeler opgedeelt. Et kéint dräi Slice Elementer vertrieden (ptr, len и cap) als Struktur (Struktur), awer representéiert se als dräi separat Entitéiten erlaabt e puer Optimisatiounen. Aner Compilere kënnen de Slice op aner Manéier vertrieden, ofhängeg vun den Uruffkonventiounen vun de Funktiounen vun der Zilplattform.

Eng aner interessant Feature vun dësem Code ass d'Benotzung vun der Instruktioun getelementptr (dacks als GEP verkierzt).

Dës Instruktioun funktionnéiert mat Zeiger a gëtt benotzt fir e Pointer op e Slice Element ze kréien. Zum Beispill, loosst eis et mat de folgende Code vergläichen, geschriwwen an C:

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

Oder mat dem folgenden Äquivalent zu dësem:

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

Déi wichtegst Saach hei ass, datt d'Instruktioune getelementptr mécht keng dereferencing Operatiounen. Et berechent just en neie Pointer baséiert op dem existente. Et kann als Instruktioune geholl ginn mul и add um Hardware Niveau. Dir kënnt méi iwwer d'GEP Instruktioune liesen hei.

Eng aner interessant Feature vun dësem Zwëschecode ass d'Benotzung vun der Instruktioun icmp. Dëst ass eng allgemeng Zweck Instruktioun déi benotzt gëtt fir ganz Zuel Vergläicher ëmzesetzen. D'Resultat vun dëser Instruktioun ass ëmmer e Wäert vun Typ i1 - logesche Wäert. An dësem Fall gëtt e Verglach mam Schlësselwuert gemaach slt (manner ënnerschriwwen wéi), well mir zwee Zuelen vergläichen, déi virdru vum Typ vertruede sinn int. Wa mir zwee onënnerschriwwe ganz Zuelen vergläichen, da géife mir benotzen icmp, an de Schlësselwuert am Verglach benotzt wier ult. Fir Floating Point Zuelen ze vergläichen, gëtt eng aner Instruktioun benotzt, fcmp, déi op eng ähnlech Manéier funktionnéiert.

Resultater

Ech gleewen datt ech an dësem Material déi wichtegst Feature vum LLVM IR ofgedeckt hunn. Natierlech gëtt et vill méi hei. Besonnesch d'Zwëschenvertriedung vum Code kann vill Annotatiounen enthalen déi Optimisatiounspassë erlaben fir verschidde Features vum Code ze berücksichtegen, deen dem Compiler bekannt ass, déi soss net am IR ausgedréckt kënne ginn. Zum Beispill ass dëst e Fändel inbounds GEP Uweisungen, oder Fändelen nsw и nuw, déi an d'Instruktioune bäigefüügt ginn add. Dat selwecht gëllt fir d'Schlësselwuert private, wat dem Optimizer beweist datt d'Funktioun déi se markéiert net vun ausserhalb vun der aktueller Kompiléierungseenheet referenzéiert gëtt. Dëst erlaabt vill interessant interprozedural Optimisatiounen wéi d'Eliminatioun vun onbenotzten Argumenter.

Dir kënnt méi iwwer LLVM liesen Dokumentatioun, op déi Dir dacks referéiert wann Dir Ären eegene LLVM-baséierte Compiler entwéckelt. Hei Leadership, déi kuckt fir e Compiler fir eng ganz einfach Sprooch z'entwéckelen. Béid dës Informatiounsquellen wäerte fir Iech nëtzlech sinn wann Dir Ären eegene Compiler erstellt.

Léif Lieser! Benotzt Dir LLVM?

LLVM aus enger Go Perspektiv

Source: will.com

Setzt e Commentaire