JIT & Deoptimization
V8'ning JIT strategiyasi: Ignition interpretator va TurboFan optimizing compiler qanday ishlashi, TurboFan tur taxminlariga asoslangan tezkor kod yozishi, guard va deoptimization (bailout) mexanizmi, va polymorphism/megamorphism kabi deoptimizatsiyaga olib boruvchi sabablar.
65-dars
JIT & Deoptimization
JIT (Just-In-Time compilation) — V8'ning JavaScript kodini ijro paytida (runtime'da), tezkor mashina koduga aylantirib borish strategiyasi. Deoptimization — V8 oldin "tez kod" deb yozib qo'ygan mashina kodidan voz kechib, qaytadan "sekin, universal" rejimga tushib ketishi.
Buni tushunish uchun avval savol: JS interpretatsiya qilinadimi yoki kompilyatsiya qilinadimi? Javob — ikkalasi ham, ketma-ket, bosqichma-bosqich. Va aynan shu jarayon JIT deyiladi.
1. Nega bu kerak bo'lgan?
Ikkita yondashuv bor edi, tarixan:
- Sof interpretatsiya (eski brauzerlar) — kod har safar qatordan-qatorga o'qilib, darhol bajarilaveradi. Oddiy, tez ishga tushadi, lekin sekin ishlaydi (har safar qayta "tushunish" kerak).
- Sof kompilyatsiya (C++ kabi) — kod oldindan, ijrodan avval, to'liq mashina koduga aylantiriladi. Ishlash tezligi a'lo, lekin kompilyatsiya vaqti uzun — va JS uchun bu mumkin emas, chunki JS'da turlar (
x— son bo'lishi mumkin, keyin string bo'lib qolishi ham mumkin) oldindan noma'lum.
V8'ning yechimi — ikkalasini birlashtirish: dastlab tez ishga tushirish uchun interpretatsiya, so'ngra **"qaysi kod ko'p marta ishlatilyapti"**ni kuzatib borib, o'sha joylarni fon rejimida optimallashtirilgan mashina koduga aylantirish. Bu strategiya — JIT.
2. V8 darajasida — ikki dvigatel: Ignition va TurboFan
V8'da JIT jarayoni ikkita asosiy komponentdan iborat:
JS kod
│
▼
[Ignition] — interpretator
│ (bytecode'ga aylantiradi, darhol bajaradi)
│ (bir vaqtda — qaysi funksiya qancha marta chaqirilganini,
│ qanday tur (type) bilan ishlayotganini KUZATIB BORADI)
▼
"Bu funksiya ko'p marta chaqirilyapti va bir xil turlar bilan ishlayapti" — signal
▼
[TurboFan] — optimizing compiler
│ (fon rejimida, ASOSIY THREAD'ni to'xtatmasdan,
│ o'sha funksiya uchun YUQORI TEZLIKDAGI mashina kodi yozadi)
▼
Optimallashtirilgan mashina kodi — endi shu funksiya shu kod orqali bajariladi
1. Ignition (interpretator)
Har qanday JS kod avval Ignition'ga tushadi. U kodni bytecode'ga aylantiradi (mashina kodi emas, oraliq, tezroq bajariladigan format) va darhol bajaradi. Bu — nega JS sahifasi darhol ishga tushishi (uzoq kompilyatsiya kutmasdan).
Ignition bajarish jarayonida "Feedback" (fikr-mulohaza) to'playdi: "bu funksiya necha marta chaqirildi?", "parametrlar qanday tur (number, string, object)da keldi?", "bu obyektning hidden class'i qaysi?" (aynan oldingi mavzuda ko'rgan narsa!).
2. TurboFan (optimizing compiler)
Agar biror funksiya "hot" (issiq — juda ko'p marta chaqirilayotgan) deb topilsa, V8 uni TurboFan'ga yuboradi. TurboFan Ignition to'plagan feedback asosida taxminlar (assumptions) qiladi:
"Bu funksiya har doim ikkita number tur bilan chaqirilgan, demak men bu haqda ishonch bilan optimallashtirilgan kod yoza olaman — turlarni har safar tekshirib o'tirmasdan."
function add(a, b) {
return a + b;
}
for (let i = 0; i < 100000; i++) {
add(i, i + 1); // har doim number bilan chaqiriladi
}
Bir necha bor number + number bilan chaqirilgandan keyin, TurboFan bu funksiya uchun faqat son qo'shishga mo'ljallangan, juda tezkor mashina kodi yozadi — typeof tekshiruvi, universal + operator mantiqi (string birlashtirishi mumkinligi kabi holatlarni tekshirish) — hammasi olib tashlanadi, chunki "baribir har doim number keladi" deb taxmin qilingan.
3. Deoptimization — taxmin noto'g'ri chiqqanda
Endi eng muhim qism. TurboFan yozgan tezkor kod — taxminga asoslangan. Agar taxmin buzilsa (masalan, funksiyaga to'satdan boshqa tur kelsa) — bu kod endi noto'g'ri natija berishi mumkin. V8 buni oldini olish uchun har bir optimallashtirilgan kod ichiga "guard" (qo'riqchi tekshiruv) qo'yadi:
add(5, 10); // number, number — optimallashtirilgan kod ishlaydi
add("a", "b"); // GUARD ishga tushadi: "bu number emas!"
Guard taxmin buzilganini payqagan zahoti, V8 deoptimizationni amalga oshiradi:
- TurboFan yozgan optimallashtirilgan mashina kodi butunlay tashlab yuboriladi
- Ijro Ignition'ga (sekin interpretator rejimiga) qaytariladi — bu jarayon "bailout" deyiladi
- Agar funksiya keyinchalik yana "hot" bo'lib qolsa, jarayon qaytadan boshidan boshlanadi — Ignition qayta feedback yig'adi, TurboFan qayta optimallashtiradi (endi ikki xil tur uchun, "polymorphic" holatda)
Bu — nega ba'zan dastur birdan sekinlashib qoladi, garchi kodning o'zi o'zgarmagan bo'lsa ham: sabab — turlar aralashib ketgani uchun V8 optimallashtirilgan kodini tashlab, orqaga qaytgan.
4. Deoptimizatsiyaga olib boradigan keng tarqalgan sabablar
1. Turlarni aralashtirish (polymorphism / megamorphism)
function qiymatOl(obj) {
return obj.x;
}
qiymatOl({ x: 1 }); // hidden class A
qiymatOl({ x: 1, y: 2 }); // hidden class B — boshqacha!
qiymatOl({ x: "matn" }); // x turi ham boshqa — number emas, string
V8 bunday holatda bir nechta hidden class va tur bilan ishlashga majbur — bu "polymorphic" (2-4 xil shape) yoki "megamorphic" (juda ko'p xil shape, odatda 5+) deb ataladi. Megamorphic holatda TurboFan optimallashtirishdan deyarli butunlay voz kechadi.
2. try/catch ichida (eski V8'larda muammo edi, hozir ancha yaxshilangan, lekin diqqat talab qiladi)
Zamonaviy V8 (2017-yildan keyingi versiyalar) try/catchni yaxshi optimallashtiradi, lekin arguments obyekti, eval(), va with kabi "dinamik" konstruksiyalar hamon optimallashtirishga to'sqinlik qiladi, chunki V8 ijro vaqtida nima bo'lishini oldindan bilolmaydi.
3. Funksiya signaturasini (parametr sonini) o'zgartirish
function f(a, b) { return a + b; }
f(1, 2); // 2 argument
f(1, 2, 3); // 3 argument — boshqacha "shape" sifatida ko'riladi
4. Array'larni "aralash tur" bilan to'ldirish
const arr = [1, 2, 3]; // "PACKED_SMI_ELEMENTS" — eng tez ichki format
arr.push(4.5); // "PACKED_DOUBLE_ELEMENTS"ga o'tadi
arr.push("matn"); // "PACKED_ELEMENTS" (universal, eng sekin)ga o'tadi
V8 massivlar uchun ham xuddi hidden class kabi, ichki elementlar formatini kuzatadi. Bir xil tur (masalan, faqat integer) bilan to'ldirilgan massiv — eng tez formatda saqlanadi. Aralash tur qo'shilishi bilan V8 massivni yomonroq (universalroq, sekinroq) formatga o'tkazadi, va bu orqaga qaytarilmaydi — hatto keyin aralash elementlar olib tashlansa ham.
Amaliy xulosa
// YOMON — turlar aralashadi, deoptimizatsiyaga olib keladi
function hisobla(x) {
return x * 2;
}
hisobla(5);
hisobla("5"); // string — endi "hisobla" polymorphic bo'lib qoladi
// YAXSHI — funksiyani bitta, izchil tur bilan chaqirish
function hisobla(x) {
return x * 2;
}
hisobla(5);
hisobla(10); // doim number — V8 monomorphic optimallashtirish qiladi
Qoida: funksiyalarni izchil turlar bilan chaqiring, obyektlarni bir xil shapeda yarating (oldingi mavzu), massivlarni bir xil tur bilan to'ldiring. Bu — "premature optimization" emas, balki V8'ning tabiiy ishlash tarzini hurmat qilish.