Go перспективасынан LLVM

Компиляторды жасау өте қиын жұмыс. Бірақ, бақытымызға орай, LLVM сияқты жобалардың дамуымен бұл мәселенің шешімі айтарлықтай жеңілдетілді, бұл тіпті бір программистке өнімділігі жағынан C тіліне жақын жаңа тілді жасауға мүмкіндік береді. LLVM-мен жұмыс істеу қиын болғандықтан, бұл жүйе аз құжаттамамен жабдықталған үлкен көлемдегі кодпен ұсынылған. Осы кемшілікті түзетуге тырысу үшін бүгін аудармасы біз жариялап отырған материалдың авторы Go бағдарламасында жазылған код мысалдарын көрсетіп, олардың алғаш рет қалай аударылғанын көрсетпекші. SSA өтіңіз, содан кейін компиляторды пайдаланып LLVM IR жүйесінде tinyGO. Түсініктемелерді түсінікті ету үшін Go SSA және LLVM IR коды осы жерде берілген түсініктемелерге қатысы жоқ нәрселерді жою үшін сәл өңделген.

Go перспективасынан LLVM

Бірінші мысал

Мен мұнда қарастыратын бірінші функция - сандарды қосудың қарапайым механизмі:

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

Бұл функция өте қарапайым және, мүмкін, ештеңе қарапайым болуы мүмкін емес. Ол келесі Go SSA кодына аударылады:

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

Бұл көріністе деректер түрінің кеңестері оң жақта орналастырылады және көп жағдайда еленбейді.

Бұл шағын мысал қазірдің өзінде SSA бір аспектісінің мәнін көруге мүмкіндік береді. Атап айтқанда, кодты SSA пішініне түрлендіру кезінде әрбір өрнек өзі құрастырылған ең қарапайым бөліктерге бөлінеді. Біздің жағдайда команда return a + b, шын мәнінде, екі операцияны білдіреді: екі санды қосу және нәтижені қайтару.

Сонымен қатар, мұнда сіз бағдарламаның негізгі блоктарын көре аласыз, бұл кодта тек бір блок бар - енгізу блогы. Төменде блоктар туралы көбірек айтатын боламыз.

Go SSA коды LLVM IR-ге оңай түрленеді:

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

Байқайтын нәрсе, мұнда әртүрлі синтаксистік құрылымдар қолданылғанымен, функцияның құрылымы негізінен өзгеріссіз қалады. LLVM IR коды C-ге ұқсас Go SSA кодынан сәл күштірек. Мұнда функция декларациясында алдымен ол қайтаратын деректер түрінің сипаттамасы бар, аргумент түрі аргумент атауының алдында көрсетіледі. Бұған қоса, IR талдауын жеңілдету үшін жаһандық нысандардың атауларының алдында белгі қойылады @, ал жергілікті атаулардың алдында таңба болады % (функция жаһандық нысан ретінде де қарастырылады).

Бұл код туралы ескеретін бір нәрсе - бұл Go түрін ұсыну шешімі intLLVM IR кодын жасағанда, компиляторға және компиляция мақсатына байланысты 32 биттік немесе 64 биттік мән ретінде ұсынылуы мүмкін . Бұл LLVM IR коды көптеген адамдар ойлағандай платформаға тәуелсіз емес екендігінің көптеген себептерінің бірі. Бір платформа үшін жасалған мұндай кодты басқа платформа үшін қабылдауға және құрастыруға болмайды (егер сіз бұл мәселені шешуге жарамды болмасаңыз). аса сақтықпен).

Тағы бір айта кететін қызықты жайт, бұл түрі i64 таңбалы бүтін сан емес: ол санның таңбасын көрсету жағынан бейтарап. Нұсқауларға байланысты ол қол қойылған және қол қойылмаған сандарды көрсете алады. Қосу операциясын көрсету жағдайында бұл маңызды емес, сондықтан таңбалы немесе таңбасыз сандармен жұмыс істеудің айырмашылығы жоқ. Мұнда мен Си тілінде таңбалы бүтін айнымалы мәннің толып кетуі анықталмаған әрекетке әкелетінін атап өткім келеді, сондықтан Clang фронтенді операцияға жалауша қосады. nsw (қол қойылған орау жоқ), бұл LLVM-ге қосу ешқашан толып кетпейтінін болжауы мүмкін екенін айтады.

Бұл кейбір оңтайландырулар үшін маңызды болуы мүмкін. Мысалы, екі мәнді қосу i16 32-биттік платформада (32-биттік регистрлері бар) диапазонда қалу үшін қосудан кейін таңбаны кеңейту операциясын қажет етеді. i16. Осыған байланысты, машина регистрінің өлшемдеріне негізделген бүтін операцияларды орындау жиі тиімдірек.

Енді осы IR кодымен не болатыны бізді қызықтырмайды. Код оңтайландырылған (бірақ біздікі сияқты қарапайым мысалда ештеңе оңтайландырылмайды) содан кейін машиналық кодқа түрлендіріледі.

Екінші мысал

Біз қарастыратын келесі мысал сәл күрделірек болады. Атап айтқанда, біз бүтін сандар бөлігін қосатын функция туралы айтып отырмыз:

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

Бұл код келесі Go SSA кодына түрлендіреді:

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

Мұнда сіз SSA пішінінде кодты көрсетуге тән басқа конструкцияларды көре аласыз. Мүмкін, бұл кодтың ең айқын ерекшелігі құрылымдық ағынды басқару командаларының жоқтығы болып табылады. Есептер ағынын басқару үшін тек шартты және шартсыз секірулер бар, ал егер бұл команданы ағынды басқару командасы ретінде қарастырсақ, қайтару командасы.

Шындығында, бұл жерде бағдарламаның бұйра жақшаларды пайдаланып блоктарға бөлінбейтініне назар аударуға болады (С тілдер тобында сияқты). Ол ассемблер тілдерін еске түсіретін белгілермен бөлінген және негізгі блоктар түрінде ұсынылған. SSA-да негізгі блоктар белгіден басталып, негізгі блокты аяқтау нұсқауларымен аяқталатын сабақтас код тізбегі ретінде анықталады, мысалы − return и jump.

Бұл кодтың тағы бір қызықты мәліметтері нұсқаулықпен ұсынылған phi. Нұсқаулар әдеттен тыс және түсіну үшін біраз уақыт кетуі мүмкін. есіңізде болсын, бұл S.S.A. Static Single Assignment сөзінің қысқартылған нұсқасы. Бұл компиляторлар пайдаланатын кодтың аралық көрінісі, онда әрбір айнымалыға тек бір рет мән беріледі. Бұл біздің функция сияқты қарапайым функцияларды көрсету үшін тамаша myAddжоғарыда көрсетілген, бірақ осы бөлімде қарастырылатын функция сияқты күрделірек функциялар үшін жарамсыз sum. Атап айтқанда, айнымалылар циклды орындау кезінде өзгереді i и n.

SSA айнымалы мәндерді бір рет тағайындаудағы шектеуді нұсқаулық деп аталатынды пайдаланып айналып өтеді phi (оның аты грек алфавитінен алынған). Шындығында, кодтың SSA көрінісі C сияқты тілдер үшін жасалуы үшін сіз кейбір айла-амалдарға жүгінуіңіз керек. Бұл нұсқауды шақыру нәтижесі айнымалының ағымдағы мәні болып табылады (i немесе n), және оның параметрлері ретінде негізгі блоктар тізімі пайдаланылады. Мысалы, мына нұсқаулықты қарастырыңыз:

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

Оның мағынасы келесідей: егер алдыңғы негізгі блок блок болса entry (енгізу), содан кейін t0 тұрақты болып табылады 0, және алдыңғы негізгі блок болса for.body, содан кейін мәнді қабылдау керек t6 осы блоктан. Мұның бәрі өте жұмбақ болып көрінуі мүмкін, бірақ бұл механизм SSA жұмысын жасайды. Адамның көзқарасы бойынша, мұның бәрі кодты түсінуді қиындатады, бірақ әрбір мәннің бір рет тағайындалуы көптеген оңтайландыруларды айтарлықтай жеңілдетеді.

Егер сіз өзіңіздің компиляторыңызды жазсаңыз, әдетте мұндай материалдармен айналысудың қажеті жоқ екенін ескеріңіз. Тіпті Clang бұл нұсқаулардың барлығын жасамайды phi, ол механизмді пайдаланады alloca (ол қарапайым жергілікті айнымалылармен жұмыс істеуге ұқсайды). Содан кейін, LLVM оңтайландыру рұқсатын іске қосқан кезде шақырылады mem2reg, нұсқаулар alloca SSA пішініне түрлендірілді. Дегенмен, TinyGo Go SSA-дан кіріс алады, ол ыңғайлы түрде SSA пішініне түрлендірілді.

Қарастырылып отырған аралық код фрагментінің тағы бір жаңалығы – кесінді элементтеріне индекс бойынша қол жеткізу адресті есептеу операциясы және алынған көрсеткішке сілтемені жою операциясы түрінде ұсынылады. Мұнда сіз IR кодына тұрақтылардың тікелей қосылуын көре аласыз (мысалы - 1:int). Функциясы бар мысалда myAdd бұл қолданылған жоқ. Енді біз бұл мүмкіндіктерді алып тастағаннан кейін, LLVM IR пішіміне түрлендіру кезінде бұл кодтың не болатынын қарастырайық:

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
}

Мұнда да бұрынғыдай басқа синтаксистік құрылымдарды қамтитын сол құрылымды көруге болады. Мысалы, қоңырауларда phi мәндер мен белгілер ауыстырылды. Дегенмен, бұл жерде ерекше назар аудару керек нәрсе бар.

Бастау үшін мұнда сіз мүлдем басқа функция қолтаңбасын көре аласыз. LLVM тілімдерге қолдау көрсетпейді, нәтижесінде оңтайландыру ретінде осы аралық кодты жасаған TinyGo компиляторы осы деректер құрылымының сипаттамасын бөліктерге бөледі. Ол үш бөлік элементін көрсете алады (ptr, len и cap) құрылым (құрылым) ретінде, бірақ оларды үш бөлек нысан ретінде көрсету кейбір оңтайландыруларға мүмкіндік береді. Басқа компиляторлар мақсатты платформа функцияларының шақыру конвенцияларына байланысты кесінді басқа жолдармен көрсетуі мүмкін.

Бұл кодтың тағы бір қызықты ерекшелігі - нұсқаулықты пайдалану getelementptr (көбінесе GEP ретінде қысқартылған).

Бұл нұсқау көрсеткіштермен жұмыс істейді және кесінді элементіне көрсеткішті алу үшін қолданылады. Мысалы, оны С тілінде жазылған келесі кодпен салыстырайық:

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

Немесе осыған келесі эквивалентпен:

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

Мұнда ең бастысы - нұсқаулар getelementptr сілтемені жою операцияларын орындамайды. Ол бар болғаны негізінде жаңа көрсеткішті есептейді. Оны нұсқаулық ретінде қабылдауға болады mul и add аппараттық деңгейде. GEP нұсқаулары туралы толығырақ оқуға болады осында.

Бұл аралық кодтың тағы бір қызықты ерекшелігі - нұсқаулықты пайдалану icmp. Бұл бүтін сандарды салыстыруды жүзеге асыру үшін пайдаланылатын жалпы мақсаттағы нұсқаулық. Бұл нұсқаудың нәтижесі әрқашан түрдің мәні болып табылады i1 — логикалық мән. Бұл жағдайда түйінді сөз арқылы салыстыру жүргізіледі slt (кіші қол қойылған), өйткені біз бұрын түрімен ұсынылған екі санды салыстырамыз int. Егер біз екі таңбасыз бүтін сандарды салыстыратын болсақ, онда біз пайдаланамыз icmp, және салыстыруда қолданылатын кілт сөз болады ult. Жылжымалы нүкте сандарын салыстыру үшін басқа нұсқау пайдаланылады, fcmp, ол ұқсас жолмен жұмыс істейді.

Нәтижелері

Мен бұл материалда LLVM IR ең маңызды мүмкіндіктерін қарастырдым деп есептеймін. Әрине, мұнда көп нәрсе бар. Атап айтқанда, кодтың аралық көрінісі компиляторға белгілі кодтың белгілі бір мүмкіндіктерін есепке алу үшін оңтайландыру өтулеріне мүмкіндік беретін көптеген аннотацияларды қамтуы мүмкін, оларды басқаша IR-де көрсету мүмкін емес. Мысалы, бұл ту inbounds GEP нұсқаулары немесе жалаушалар nsw и nuw, оны нұсқауларға қосуға болады add. Бұл кілт сөзге де қатысты private, оңтайландырушыға ол белгілейтін функцияға ағымдағы жинақтау бірлігінен тыс сілтеме жасалмайтынын көрсетеді. Бұл пайдаланылмаған аргументтерді жою сияқты көптеген қызықты процедурааралық оңтайландыруларға мүмкіндік береді.

LLVM туралы қосымша ақпаратты мына жерден оқи аласыз құжаттама, сіз өзіңіздің LLVM негізіндегі компиляторыңызды жасағанда жиі сілтеме жасайтын боласыз. Мұнда көшбасшылық, ол өте қарапайым тілге арналған компиляторды әзірлеуді қарастырады. Бұл ақпарат көздерінің екеуі де өзіңіздің компиляторыңызды жасаған кезде сізге пайдалы болады.

Құрметті оқырмандар! Сіз LLVM пайдаланасыз ба?

Go перспективасынан LLVM

Ақпарат көзі: www.habr.com

пікір қалдыру