Go nuqtai nazaridan LLVM

Kompilyatorni ishlab chiqish juda qiyin ish. Ammo, xayriyatki, LLVM kabi loyihalarning rivojlanishi bilan bu muammoni hal qilish ancha soddalashtirildi, bu hatto bitta dasturchiga ham C ga yaqin bo'lgan yangi tilni yaratishga imkon beradi. LLVM bilan ishlash, bu haqiqat bilan murakkablashadi. tizim kichik hujjatlar bilan jihozlangan katta miqdordagi kod bilan ifodalanadi. Ushbu kamchilikni tuzatishga harakat qilish uchun bugungi kunda biz tarjimasi e'lon qilinayotgan material muallifi Go'da yozilgan kod misollarini ko'rsatmoqchi va ular birinchi marta qanday tarjima qilinganligini ko'rsatmoqchi. SSAga boring, va keyin kompilyator yordamida LLVM IR da tinyGO. Go SSA va LLVM IR kodi tushuntirishlarni yanada tushunarli qilish uchun bu yerda berilgan tushuntirishlarga tegishli bo'lmagan narsalarni olib tashlash uchun biroz tahrirlangan.

Go nuqtai nazaridan LLVM

Birinchi misol

Men bu erda ko'rib chiqmoqchi bo'lgan birinchi funktsiya raqamlarni qo'shishning oddiy mexanizmi:

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

Bu funktsiya juda oddiy va, ehtimol, hech narsa oddiyroq bo'lishi mumkin emas. U quyidagi Go SSA kodiga tarjima qilinadi:

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

Ushbu ko'rinishda ma'lumotlar turi bo'yicha maslahatlar o'ng tomonda joylashgan va ko'p hollarda e'tiborsiz qolishi mumkin.

Ushbu kichik misol allaqachon SSAning bir jihatining mohiyatini ko'rishga imkon beradi. Ya'ni, kodni SSA shakliga o'tkazishda har bir ifoda o'zi tuzilgan eng elementar qismlarga bo'linadi. Bizning holatda, buyruq return a + b, aslida, ikkita operatsiyani ifodalaydi: ikkita raqamni qo'shish va natijani qaytarish.

Bundan tashqari, bu erda siz dasturning asosiy bloklarini ko'rishingiz mumkin, bu kodda faqat bitta blok mavjud - kirish bloki. Quyida bloklar haqida ko'proq gaplashamiz.

Go SSA kodi osongina LLVM IR ga aylanadi:

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

E'tibor bergan narsa shundaki, bu erda turli xil sintaktik tuzilmalar qo'llanilsa-da, funktsiyaning tuzilishi asosan o'zgarmagan. LLVM IR kodi C ga o'xshash Go SSA kodidan biroz kuchliroqdir. Bu yerda funksiya deklaratsiyasida birinchi navbatda u qaytaradigan ma'lumotlar turi tavsifi mavjud, argument nomidan oldin argument turi ko'rsatilgan. Bundan tashqari, IR tahlilini soddalashtirish uchun global ob'ektlarning nomlari oldidan belgi qo'yiladi @, va mahalliy nomlardan oldin belgi mavjud % (funksiya ham global ob'ekt hisoblanadi).

Ushbu kod haqida e'tiborga olish kerak bo'lgan narsa shundaki, Go turini ko'rsatish qarori intLLVM IR kodini yaratganda, kompilyatorga va kompilyatsiya maqsadiga qarab 32-bit yoki 64-bit qiymat sifatida ifodalanishi mumkin. Bu LLVM IR kodi, ko'pchilik o'ylagandek, platformadan mustaqil emasligining ko'pgina sabablaridan biridir. Bitta platforma uchun yaratilgan bunday kodni boshqa platforma uchun olib bo'lmaydi (agar siz ushbu muammoni hal qilish uchun mos bo'lmasangiz) juda ehtiyotkorlik bilan).

E'tiborga loyiq yana bir qiziq jihat shundaki, bu tur i64 ishorali butun son emas: sonning belgisini ifodalash nuqtai nazaridan neytral. Ko'rsatmaga qarab, u imzolangan va imzosiz raqamlarni ifodalashi mumkin. Qo'shish operatsiyasini ifodalashda bu muhim emas, shuning uchun imzolangan yoki imzosiz raqamlar bilan ishlashda farq yo'q. Bu erda shuni ta'kidlashni istardimki, C tilida imzolangan butun o'zgaruvchining to'lib ketishi aniqlanmagan xatti-harakatlarga olib keladi, shuning uchun Clang frontend operatsiyaga bayroq qo'shadi. nsw (imzolangan o'ram yo'q), bu LLVM ga qo'shimcha hech qachon to'lib ketmasligini taxmin qilishi mumkinligini aytadi.

Bu ba'zi optimallashtirishlar uchun muhim bo'lishi mumkin. Masalan, ikkita qiymat qo'shish i16 32-bitli platformada (32-bitli registrlar bilan) qo'shilgandan so'ng, diapazonda qolish uchun belgini kengaytirish operatsiyasini talab qiladi. i16. Shu sababli, ko'pincha mashina registrlari o'lchamlari asosida butun son operatsiyalarini bajarish samaraliroq bo'ladi.

Ushbu IR kodi bilan keyin nima sodir bo'lishi bizni hozir qiziqtirmaydi. Kod optimallashtiriladi (lekin biznikiga o'xshash oddiy misolda, hech narsa optimallashtirilmaydi) va keyin mashina kodiga aylantiriladi.

Ikkinchi misol

Biz ko'rib chiqadigan keyingi misol biroz murakkabroq bo'ladi. Ya'ni, biz butun sonlar bo'lagini yig'adigan funktsiya haqida gapiramiz:

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

Ushbu kod quyidagi Go SSA kodiga aylanadi:

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

Bu erda siz SSA shaklida kodni ifodalash uchun odatiy bo'lgan ko'proq konstruktsiyalarni ko'rishingiz mumkin. Ehtimol, ushbu kodning eng aniq xususiyati - oqimlarni boshqarish buyruqlari mavjud emasligi. Hisob-kitoblar oqimini boshqarish uchun faqat shartli va shartsiz sakrashlar mavjud va agar bu buyruqni oqimni boshqarish buyrug'i deb hisoblasak, qaytarish buyrug'i.

Darhaqiqat, bu erda siz dasturning jingalak qavslar yordamida bloklarga bo'linmaganligiga e'tibor berishingiz mumkin (C oilasida bo'lgani kabi). U montaj tillarini eslatuvchi teglar bilan bo'linadi va asosiy bloklar shaklida taqdim etiladi. SSAda asosiy bloklar yorliqdan boshlanib, blokni yakunlash bo'yicha asosiy ko'rsatmalar bilan tugaydigan ketma-ket kodlar sifatida aniqlanadi, masalan - return ΠΈ jump.

Ushbu kodning yana bir qiziqarli tafsiloti ko'rsatma bilan ifodalanadi phi. Ko'rsatmalar juda g'ayrioddiy va tushunish biroz vaqt talab qilishi mumkin. esda tut, shuni SSA Statik yagona tayinlashning qisqartmasi. Bu kompilyatorlar tomonidan ishlatiladigan kodning oraliq ko'rinishi bo'lib, unda har bir o'zgaruvchiga faqat bir marta qiymat beriladi. Bu bizning funktsiyamiz kabi oddiy funktsiyalarni ifodalash uchun juda yaxshi myAddyuqorida ko'rsatilgan, lekin bu bo'limda muhokama qilingan funksiya kabi murakkabroq funksiyalar uchun mos emas sum. Xususan, siklning bajarilishi jarayonida o'zgaruvchilar o'zgaradi i ΠΈ n.

SSA ko'rsatma deb ataladigan narsadan foydalanib, bir marta o'zgaruvchan qiymatlarni belgilash cheklovini chetlab o'tadi phi (uning nomi yunon alifbosidan olingan). Gap shundaki, kodning SSA ko'rinishi C kabi tillar uchun yaratilishi uchun siz ba'zi hiyla-nayranglarga murojaat qilishingiz kerak. Ushbu ko'rsatmani chaqirish natijasi o'zgaruvchining joriy qiymati (i yoki n), va uning parametrlari sifatida asosiy bloklar ro'yxati ishlatiladi. Masalan, ushbu ko'rsatmani ko'rib chiqing:

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

Uning ma'nosi quyidagicha: agar oldingi asosiy blok blok bo'lsa entry (kirish), keyin t0 doimiy hisoblanadi 0, va agar oldingi asosiy blok bo'lsa for.body, keyin siz qiymatni olishingiz kerak t6 ushbu blokdan. Bularning barchasi juda sirli bo'lib tuyulishi mumkin, ammo bu mexanizm SSA ishlashiga imkon beradi. Inson nuqtai nazaridan, bularning barchasi kodni tushunishni qiyinlashtiradi, lekin har bir qiymat faqat bir marta tayinlanganligi ko'plab optimallashtirishni ancha osonlashtiradi.

E'tibor bering, agar siz o'zingizning kompilyatoringizni yozsangiz, odatda bunday narsalar bilan shug'ullanishingiz shart emas. Hatto Clang ham bu ko'rsatmalarni yaratmaydi phi, u mexanizmdan foydalanadi alloca (oddiy mahalliy o'zgaruvchilar bilan ishlashga o'xshaydi). Keyin, LLVM optimallashtirish passini ishga tushirganda chaqiriladi mem2reg, ko'rsatmalar alloca SSA shakliga aylantirildi. Biroq, TinyGo Go SSA-dan ma'lumot oladi, u qulay tarzda allaqachon SSA shakliga aylantirilgan.

Ko'rib chiqilayotgan oraliq kod fragmentining yana bir yangiligi shundan iboratki, tilim elementlariga indeks bo'yicha kirish manzilni hisoblash operatsiyasi va natijada ko'rsatgichga havolani olib tashlash operatsiyasi ko'rinishida taqdim etiladi. Bu erda siz IR kodiga doimiylarning to'g'ridan-to'g'ri qo'shilishini ko'rishingiz mumkin (masalan - 1:int). Funktsiya bilan misolda myAdd bu ishlatilmagan. Endi biz ushbu xususiyatlardan voz kechdik, keling, ushbu kod LLVM IR shakliga aylantirilganda nima bo'lishini ko'rib chiqaylik:

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
}

Bu yerda, avvalgidek, boshqa sintaktik tuzilmalarni o'z ichiga olgan bir xil tuzilmani ko'rishimiz mumkin. Masalan, qo'ng'iroqlarda phi qiymatlar va teglar almashtirildi. Biroq, bu erda alohida e'tibor berishga arziydigan narsa bor.

Boshlash uchun, bu erda siz butunlay boshqa funktsiya imzosini ko'rishingiz mumkin. LLVM tilimlarni qo'llab-quvvatlamaydi va natijada optimallashtirish sifatida ushbu oraliq kodni yaratgan TinyGo kompilyatori ushbu ma'lumotlar strukturasi tavsifini qismlarga ajratdi. U uchta bo'lak elementini ifodalashi mumkin (ptr, len ΠΈ cap) tuzilma (struktura) sifatida, lekin ularni uchta alohida ob'ekt sifatida ifodalash ba'zi optimallashtirishlarga imkon beradi. Boshqa kompilyatorlar maqsadli platforma funksiyalarining chaqiruv konventsiyalariga qarab, tilimni boshqa yo'llar bilan ifodalashi mumkin.

Ushbu kodning yana bir qiziqarli xususiyati - bu ko'rsatmalardan foydalanish getelementptr (ko'pincha GEP deb qisqartiriladi).

Ushbu ko'rsatma ko'rsatkichlar bilan ishlaydi va tilim elementiga ko'rsatgichni olish uchun ishlatiladi. Masalan, uni C da yozilgan quyidagi kod bilan solishtiramiz:

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

Yoki bunga quyidagi ekvivalent bilan:

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

Bu erda eng muhimi, ko'rsatmalar getelementptr havoladan chiqarish operatsiyalarini bajarmaydi. U faqat mavjud bo'lgan yangi ko'rsatkichni hisoblab chiqadi. Buni ko'rsatmalar sifatida qabul qilish mumkin mul ΠΈ add apparat darajasida. GEP ko'rsatmalari haqida ko'proq o'qishingiz mumkin shu yerda.

Ushbu oraliq kodning yana bir qiziqarli xususiyati - bu ko'rsatmadan foydalanish icmp. Bu butun sonlarni taqqoslash uchun ishlatiladigan umumiy maqsadli ko'rsatma. Ushbu ko'rsatmaning natijasi har doim turdagi qiymatdir i1 - mantiqiy qiymat. Bunday holda, kalit so'z yordamida taqqoslash amalga oshiriladi slt (kamroq imzolangan), chunki biz ilgari tur bo'yicha ifodalangan ikkita raqamni solishtiramiz int. Agar biz ikkita belgisiz butun sonni taqqoslagan bo'lsak, unda biz foydalanamiz icmp, va taqqoslashda ishlatiladigan kalit so'z bo'ladi ult. O'zgaruvchan nuqta raqamlarini solishtirish uchun boshqa ko'rsatma ishlatiladi, fcmp, shunga o'xshash tarzda ishlaydi.

natijalar

Men ushbu materialda LLVM IR ning eng muhim xususiyatlarini yoritganimga ishonaman. Albatta, bu erda juda ko'p narsa bor. Xususan, kodning oraliq tasviri kompilyatorga ma'lum bo'lgan kodning ba'zi xususiyatlarini inobatga olish uchun optimallashtirish o'tishlariga imkon beruvchi ko'plab izohlarni o'z ichiga olishi mumkin. Masalan, bu bayroq inbounds GEP ko'rsatmalari yoki bayroqlar nsw ΠΈ nuw, bu ko'rsatmalarga qo'shilishi mumkin add. Xuddi shu narsa kalit so'z uchun ham amal qiladi private, optimallashtiruvchiga u belgilagan funksiya joriy kompilyatsiya birligidan tashqarida havola qilinmasligini bildiradi. Bu foydalanilmagan argumentlarni yo'q qilish kabi ko'plab qiziqarli protseduralararo optimallashtirish imkonini beradi.

LLVM haqida ko'proq o'qishingiz mumkin hujjatlar, siz o'zingizning LLVM-ga asoslangan kompilyatoringizni ishlab chiqishda tez-tez murojaat qilasiz. Bu yerga etakchilik, bu juda oddiy til uchun kompilyatorni ishlab chiqishga qaraydi. O'zingizning kompilyatoringizni yaratishda ushbu ikkala ma'lumot manbasi sizga foydali bo'ladi.

Hurmatli o'quvchilar! LLVM dan foydalanasizmi?

Go nuqtai nazaridan LLVM

Manba: www.habr.com

a Izoh qo'shish