Kodėl „JavaScript“ lyginamoji analizė yra netvarka?

Nekenčiu palyginimo kodo, kaip ir bet kuris žmogus (kuriu šiuo metu dauguma žiūrinčiųjų tikriausiai nėra ¯\(ツ)/¯). Daug smagiau apsimesti, kad vertės išsaugojimas talpykloje padidino našumą 1000%, o ne išbandyti, ką ji padarė. Deja, „JavaScript“ lyginamoji analizė vis dar būtina, ypač dėl to, kad „JavaScript“ naudojama (kai to neturėtų būti?) našumui jautresnėse programose. Deja, dėl daugelio pagrindinių architektūrinių sprendimų „JavaScript“ nepalengvina lyginamosios analizės.
Kas negerai su JavaScript?
JIT kompiliatorius padidina tikslumą (?)
Tiems, kurie nėra susipažinę su šiuolaikinių scenarijų kalbų, tokių kaip „JavaScript“, magija, jų architektūra gali būti gana sudėtinga. Užuot paleidę kodą tik per vertėją, kuris iš karto išsiunčia instrukcijas, dauguma „JavaScript“ variklių naudoja architektūrą, panašesnę į kompiliuojamą kalbą, pvz., C – jie integruoja kelis „kompiliatorių“ lygius.
Kiekvienas iš šių kompiliatorių siūlo skirtingą kompiliavimo laiko ir vykdymo laiko našumo kompromisą, todėl vartotojui nereikia išleisti skaičiavimo optimizavimo kodo, kuris retai paleidžiamas, tuo pačiu pasinaudojant pažangesnio kompiliatoriaus našumo pranašumais dažniausiai paleidžiamam kodui ( „karštieji keliai“). Taip pat yra keletas kitų komplikacijų, kurios iškyla naudojant optimizavimo kompiliatorius, kurie apima tokius įmantrius programavimo žodžius kaip „funkcijos monomorfizmas“, bet aš jus pasigailėsiu ir vengsiu apie tai kalbėti čia.
Taigi… kodėl tai svarbu atliekant lyginamąją analizę? Na, kaip jau galėjote atspėti, nes lyginamoji analizė matuoja pasirodymas kodo, JIT kompiliatorius gali turėti gana didelę įtaką. Mažesnių kodo dalių palyginimas dažnai gali pastebėti 10 kartų ir daugiau našumo patobulinimų po visiško optimizavimo, todėl rezultatuose atsiranda daug klaidų.
Pavyzdžiui, paprasčiausioje palyginimo sąrankoje (nenaudokite nieko panašaus į toliau pateiktą dėl kelių priežasčių):
for (int i = 0; i<1000; i++) {
console.time()
// do some expensive work
console.timeEnd()
}
(Nesijaudinkite, mes kalbėsime console.time
taip pat)
Po kelių bandymų daug kodo bus išsaugota talpykloje, todėl operacijos laikas žymiai sutrumpės. Lyginamosios programos dažnai daro viską, kad pašalintų šį talpyklą / optimizavimą, nes dėl to vėliau palyginimo proceso metu išbandytos programos gali pasirodyti santykinai greičiau. Tačiau galiausiai turite paklausti, ar gairės be optimizavimo atitinka našumą realiame pasaulyje.
Žinoma, tam tikrais atvejais, pavyzdžiui, retai lankomuose tinklalapiuose, optimizavimas yra mažai tikėtinas, tačiau tokiose aplinkose kaip serveriai, kur našumas yra svarbiausias, optimizavimo reikėtų tikėtis. Jei naudojate kodo dalį kaip tarpinę programinę įrangą tūkstančiams užklausų per sekundę, geriau tikėkitės, kad V8 jį optimizuos.
Taigi iš esmės net viename variklyje yra 2–4 skirtingi kodo paleidimo būdai, kurių našumas skiriasi. Be to, tam tikrais atvejais yra nepaprastai sunku užtikrinti, kad būtų įjungti tam tikri optimizavimo lygiai. smagiai :).
Varikliai daro viską, kad neleistų jums tiksliai nustatyti laiko
Žinai pirštų atspaudus? Technika, kuri leido naudoti „Do Not Track“. pagalba sekimas? Taip, „JavaScript“ varikliai padarė viską, kad tai sušvelnintų. Šios pastangos ir pastangos užkirsti kelią laiko atakoms lėmė, kad „JavaScript“ varikliai tyčia padarė netikslų laiką, todėl įsilaužėliai negali gauti tikslių dabartinių kompiuterių našumo matavimų arba tam tikros operacijos brangumo.
Deja, tai reiškia, kad nekeičiant dalykų, etalonai turi tą pačią problemą.
Ankstesniame skyriuje pateiktas pavyzdys bus netikslus, nes matuojamas tik milisekundėmis. Dabar išjunkite tai performance.now()
. Puiku.
Dabar turime laiko žymes mikrosekundėmis!
// Bad
console.time();
// work
console.timeEnd();
// Better?
const t = performance.now();
// work
console.log(performance.now() - t);
Išskyrus… jie visi yra 100 μs žingsniais. Dabar pridėkime keletą antraščių, kad sumažintume laiko atakų riziką. Oi, vis tiek galime padidinti tik 5 μs. 5 μs tikriausiai yra pakankamas tikslumas daugeliui naudojimo atvejų, tačiau turėsite ieškoti kitur visko, kas reikalauja daugiau detalumo. Kiek žinau, jokia naršyklė neleidžia naudoti detalesnių laikmačių. Node.js tai daro, bet, žinoma, tai turi savo problemų.
Net jei nuspręsite paleisti kodą per naršyklę ir leisite kompiliatoriui atlikti savo darbą, aišku, jums vis tiek skaudės galvą, jei norite tikslaus laiko. O taip, ir ne visos naršyklės yra vienodos.
Kiekviena aplinka yra skirtinga
Man patinka „Bun“ už tai, ką ji padarė, kad pastūmėtų serverio „JavaScript“, bet, deja, dėl to serverių „JavaScript“ palyginimas yra daug sunkesnis. Prieš kelerius metus vienintelės serverio pusės „JavaScript“ aplinkos, kurios žmonėms rūpėjo, buvo „Node.js“ ir „Deno“, kurios abi naudojo V8 „JavaScript“ variklį (tą patį „Chrome“). Vietoj to „Bun“ naudoja „JavaScriptCore“, „Safari“ variklį, kurio veikimo charakteristikos visiškai skiriasi.
Ši kelių „JavaScript“ aplinkų, turinčių savo našumo charakteristikas, problema yra palyginti nauja serverio pusės „JavaScript“, tačiau ji ilgą laiką kankino klientus. 3 skirtingi dažniausiai naudojami „JavaScript“ varikliai, atitinkamai V8, JSC ir „SpiderMonkey“, skirti „Chrome“, „Safari“ ir „Firefox“, gali veikti žymiai greičiau arba lėčiau naudojant lygiavertį kodo fragmentą.
Vienas iš šių skirtumų pavyzdžių – tail Call Optimization (TCO). TCO optimizuoja funkcijas, kurios pasikartoja jų kūno gale, pavyzdžiui:
function factorial(i, num = 1) {
if (i == 1) return num;
num *= i;
i--;
return factorial(i, num);
}
Pabandykite atlikti lyginamąją analizę factorial(100000)
in Bun. Dabar išbandykite tą patį „Node.js“ arba „Deno“. Turėtumėte gauti klaidą, panašią į šią:
function factorial(i, num = 1) {
^
RangeError: Maximum call stack size exceeded
V8 (ir pagal plėtinį Node.js ir Deno) kiekvieną kartą factorial()
pabaigoje iškviečia save, variklis sukuria visiškai naują funkcijos kontekstą įdėtai funkcijai, kurią galiausiai riboja iškvietimų krūva. Bet kodėl tai neįvyksta Bune? „JavaScriptCore“, kurią naudoja „Bun“, įgyvendina TCO, kuri optimizuoja šių tipų funkcijas, paversdama jas „for“ ciklais, panašiais į šį:
function factorial(i, num = 1) {
while (i != 1) {
num *= i;
i--;
}
return i;
}
Aukščiau pateikta konstrukcija ne tik leidžia išvengti iškvietimų krūvos apribojimų, bet ir yra daug greitesnė, nes nereikalauja jokių naujų funkcijų kontekstų, o tai reiškia, kad tokios funkcijos kaip pirmiau pateiktos skirtinguose varikliuose bus vertinamos labai skirtingai.
Iš esmės šie skirtumai reiškia, kad turėtumėte palyginti visus variklius, kuriuose tikitės, kad jūsų kodas bus paleistas, kad įsitikintumėte, jog vienas greitas kodas kitame nėra lėtas. Be to, jei kuriate biblioteką, kurią tikitės naudoti daugelyje platformų, būtinai įtraukite daugiau ezoterinių variklių, tokių kaip Hermes; jie turi drastiškai skirtingas veikimo charakteristikas.
Garbingi paminėjimai
- Šiukšlių surinkėjas ir jo polinkis viską pristabdyti atsitiktinai.
- JIT kompiliatoriaus galimybė ištrinti visą jūsų kodą, nes tai „nebūtina“.
- Siaubingai plačios liepsnos diagramos daugelyje „JavaScript“ kūrimo įrankių.
- Manau, kad supratai esmę.
Taigi… Kas yra sprendimas?
Norėčiau, kad galėčiau nurodyti npm paketą, kuris išsprendžia visas šias problemas, bet tokio tikrai nėra.
Serveryje jums šiek tiek lengviau. Naudodami d8 galite rankiniu būdu valdyti optimizavimo lygius, valdyti šiukšlių rinktuvą ir gauti tikslų laiką. Žinoma, jums reikės šiek tiek Bash-fu, kad sukurtumėte gerai suplanuotą etaloninį vamzdyną, nes, deja, d8 nėra gerai integruotas (arba iš viso nėra integruotas) su Node.js.
Taip pat galite įgalinti tam tikras žymes Node.js, kad gautumėte panašių rezultatų, bet praleisite tokias funkcijas kaip konkrečių optimizavimo pakopų įgalinimas.
v8 --sparkplug --always-sparkplug --no-opt (file)
D8 pavyzdys, kai įgalinta konkreti kompiliavimo pakopa (kibirkštis). D8 pagal numatytuosius nustatymus apima daugiau GC valdymo ir daugiau derinimo informacijos apskritai.
Galite gauti keletą panašių funkcijų „JavaScriptCore“? Sąžiningai, aš mažai naudoju JavaScriptCore CLI, ir taip yra stipriai nepakankamai dokumentuotas. Galite įjungti konkrečias pakopas naudodami komandų eilutės vėliavėles, bet nesu tikras, kiek derinimo informacijos galite gauti. „Bun“ taip pat apima keletą naudingų palyginimo paslaugų, tačiau jos yra ribojamos panašiai kaip „Node.js“.
Deja, visa tai reikalauja bazinio variklio / bandomosios variklio versijos, kurią gali būti gana sunku gauti. Pastebėjau, kad paprasčiausias būdas valdyti variklius yra esvu suporuotas su eshost-cli, nes jie kartu žymiai palengvina variklių valdymą ir kodo paleidimą juose. Žinoma, vis dar reikia daug rankinio darbo, nes šie įrankiai tiesiog valdo paleidimo kodą skirtinguose varikliuose – jūs vis tiek turite patys parašyti lyginamosios analizės kodą.
Jei tiesiog bandote kuo tiksliau lyginti variklį su numatytosiomis parinktimis serveryje, yra paruoštų Node.js įrankių, pvz., mitata, kurie padeda pagerinti laiko nustatymo tikslumą ir su GC susijusias klaidas. Daugelis šių įrankių, pvz., Mitata, taip pat gali būti naudojami daugelyje variklių; žinoma, jūs vis tiek turėsite nustatyti dujotiekį, kaip nurodyta aukščiau.
Naršyklėje viskas yra daug sudėtingiau. Tikslesnio laiko nustatymo sprendimų nežinau, o variklio valdymas yra kur kas ribotesnis. Daugiausia informacijos, susijusios su vykdymo laiko „JavaScript“ našumu naršyklėje, gausite iš „Chrome“ kūrėjų įrankių, kuriuose siūlomos pagrindinės liepsnos diagramos ir procesoriaus lėtėjimo modeliavimo priemonės.
Išvada
Dėl daugelio tų pačių dizaino sprendimų, dėl kurių „JavaScript“ (santykinai) buvo našus ir nešiojamas, lyginamoji analizė yra žymiai sunkesnė nei kitomis kalbomis. Yra daug daugiau tikslų, kuriuos reikia palyginti, ir jūs turite daug mažiau galimybių valdyti kiekvieną tikslą.
Tikimės, kad sprendimas kada nors supaprastins daugelį šių problemų. Galų gale galėčiau sukurti įrankį, skirtą supaprastinti kelių variklių ir kompiliavimo pakopų lyginamąją analizę, tačiau kol kas norint išspręsti visas šias problemas, reikia nemažai padirbėti. Žinoma, svarbu atsiminti, kad šios problemos galioja ne visiems – jei jūsų kodas veikia tik vienoje aplinkoje, nešvaistykite laiko kitų aplinkų palyginimui.
Kad ir kaip pasirinktumėte palyginimą, tikiuosi, kad šiame straipsnyje buvo parodytos kai kurios „JavaScript“ palyginimo problemos. Praneškite man, jei būtų naudinga pamoka, kaip įgyvendinti kai kuriuos anksčiau aprašytus dalykus.