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.
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 int
LLVM 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)
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 myAdd
yuqorida 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 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
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
Hurmatli o'quvchilar! LLVM dan foydalanasizmi?
Manba: www.habr.com