Baitkodo VM
Numatytasis vykdymo kelias yra medžio vaikščiotuvas — pereinama per kiekvieną AST mazgą ir grąžinama reikšmė. Aktyvioms funkcijoms, iškviečiamoms milijonus kartų, kliūtimi tampa kiekvieno mazgo iškvietimų apdorojimo sąnaudos. Phasis šalia prideda spekuliatyvų baitkodo kompiliatorių, kuris kodą paverčia PHP uždarymais, kad šios sąnaudos sumažėtų.
Kada įsijungia VM
Kiekviena JsFunction turi aktyvių iškvietimų skaitiklį. Jam viršijus slenkstį (numatytoji reikšmė 100), variklis bando funkcijos turinį per src/Bytecode/JsToPhp.php kompiliuoti į PHP uždarymą.
Kompiliuoti pavyksta, kai funkcijos turinys atitinka vieną iš žinomų formų:
- Nuspėjamų tipų vietinių kintamųjų priskyrimai (skaitinių reikšmių, objektų, funkcijų, masyvų, eilučių).
- Skaičių aritmetika.
- Objektinio tipo vietinių kintamųjų IR
this(this.v,obj.x) elementų skaitymas. - Objektinio tipo vietinių kintamųjų metodų iškvietimai (
obj.method(args)), gavėją perduodant kaip$thisValue. new Ctor(args)konstruktoriaus iškvietimai — LHS vietiniam kintamajam priskiriamasobjecttipas, o ne objektinės grąžinamos reikšmės atveju aktyvinama kompiliavimo atsisakymo apsauga.for/whileciklai su paprastomis sąlygomis.returnsu apskaičiuojama išraiška.if/else, kurių sąlygos yra tipizuotos.
Kompiliuoti nepavyksta (funkcija lieka medžio vaikščiotuve), jei turinyje yra:
withsakinių.try / catch / finally(tinkamą užbaigimo semantiką užtikrina medžio vaikščiotuvas).eval()iškvietimų.- Tiesioginis
argumentskeitimas. - Bet kas, ko kompiliatorius dar nemoka paversti žemesnio lygmens kodu.
Kai kompiliuoti pavyksta, variklis pakeičia funkcijos iškvietimo kelią į kompiliuotą uždarymą. Vėlesni iškvietimai vykdo savąjį PHP kodą, patirdami vieno funkcijos iškvietimo sąnaudas, o ne sąnaudas už kiekvieną AST mazgą.
Kompiliavimo atsisakymas
Kompiliuotas greitasis kelias daro prielaidas (skaičiai lieka skaičiais, savybės išlieka tos pačios formos). Kai vykdymo metu prielaida pažeidžiama, uždarymas išmeta Bailout, o vykdymas tam iškvietimui pradedamas iš naujo medžio vaikščiotuve.
Čia specifikacijos laikymasis svarbiausias. Medžio vaikščiotuvas turi galėti nuo pradžių pakartotinai vykdyti kompiliavimo atsisakymą sukėlusį iškvietimą, nepastebėdamas jokių nepavykusio greitojo kelio šalutinių poveikių. Phasis tai užtikrina dviem būdais:
- Šalutinio poveikio neturinčios pradžios dalys — kompiliatorius prieš bet kokias spekuliacijas ieško šalutinių poveikių. Jei funkcija iki spekuliavimo taško atlieka įvesties ar išvesties operacijas (console.log, pagrindinės aplinkos funkcijas), greitasis kelias jų nepraleidžia, o patvirtina ir tęsia vykdymą.
- Atsisakymas kompiliuoti — kai formai reikėtų atšaukti pastebimus šalutinius poveikius (ne skaitinė
let r = fn(), kuriojefn()keičia būseną), kompiliatorius atsisako ją kompiliuoti. Funkcija lieka medžio vaikščiotuve.
Pagrindinis pavyzdys: greitajame kelyje atliekamas nepriskaitinis priskyrimas, vykdomas fn(), išmetama Bailout, o tada medžio vaikščiotuvas jį vykdo iš naujo — fn() paleidžiama du kartus ir sukeliami du šalutinių poveikių rinkiniai. Atsisakant kompiliuoti užtikrinama, kad fn() apskritai nebus vykdomas greitajame kelyje, todėl medžio vaikščiotuvas matys tik vieną iškvietimą.
VM iškvietimų apdorojimas (antrasis sluoksnis)
Kai kurios funkcijos kompiliuojamos į PHP uždarymus, o kitos nukreipiamos į dar paprastesnį baitkodo interpretatorių, esantį src/Bytecode/VM.php. Baitkodas yra plokščias operacijų kodų masyvas (LOAD_LOCAL, STORE_PROP, CALL_METHOD, …), kurį apdoroja switch.
VM naudojama funkcijoms, kurių neapsimoka kompiliuoti į PHP uždarymą, bet kurios vis tiek pakankamai dažnai iškviečiamos, kad būtų naudinga praleisti AST perėjimą. Įtaisytųjų objektų (Array.prototype.map, String.prototype.split, …) metodų iškvietimai apdorojami VM greituosiuose keliuose, kurie apeina bendrąjį callFunction tarpinį iškvietimą.
Tinkinta iškvietimų steko realizacija
Op::CALL, Op::CALL_METHOD ir Op::NEW_CALL naudoja įterptinių iškvietimų kelią, kuriame visas JS iškvietimų stekas laikomas VM::execute viduje — kiekvienam JS iškvietimui nesukuriamas naujas PHP kadras. Įeinant iškvietusiojo būsena (pc, kodo srautas, operandų stekas, vietiniai kintamieji, env, this, griežtojo režimo vėliavėlė) įrašoma į kiekvienos VM savedFrames telkinį, kuriame laikomi SavedFrame egzemplioriai, o iškvietimų ciklo vietiniai kintamieji perkonfigūruojami pagal iškviečiamą funkciją. Op::RET išima išsaugotą kadrą ir atkuria būseną. Išimtys išvynioja išsaugotus kadrus, o tada vykdymas grąžinamas medžio vaikščiotuvui.
Tinkamumas įsimenamas JsFunction: vmInlineCallCache įprastiems iškvietimams (baitkodu sukompiliuotas turinys, nesusietas su bind(), nesavasis), vmInlineConstructCache, skirtiems new išraiškoms (papildomai isConstructable, ne išvestinis, be egzempliorių laukų ar privačių metodų inicializatorių). Visa kita apdorojama įprastu callFunction / vmNewExpression tarpiniu iškvietimu.
Architektūrinis pranašumas: PHP steko riba neberiboja JS rekursijos gylio. Dabar vienintelė riba yra CallStack::maxDepth — nustatyta į 4 096. Ji gerokai aukštesnė už visų teisėtų test262 atvejų poreikius (daugumai reikia mažiau nei 2 kadrų) ir pakankamai žema, kad patologiniai begalinės rekursijos bandymai greitai pasiektų vykdyklės skirtąjį laiką. Transpiliuojamų uždarymų kelias (JsToPhp) turi atskirą 8 192 kadrų apsaugą atsarginiam keliui, kai iškvietimai neįterpiami.
Generatoriai su kadrų momentinėmis kopijomis
Paprastų generatorių turiniai — be yield*, be yield bloke try/catch/finally ir be await — kompiliuojami į baitkodą. Kompiliatorius sustabdymo vietoje pateikia naują Op::YIELD operacijos kodą; Op::YIELD įrašo iškvietimų būseną (cf, pc+1, stekas, sp, vietiniai kintamieji, env, thisValue, strict) į GeneratorSnapshot krūvos objektą ir iš VM::execute grąžina YieldResult žymeklį. Vykdantysis JsGenerator saugo momentinę kopiją ir kito next() / throw() metu grąžina ją į VM::execute, pakeisdamas viršuje esantį operandų steko reikšmę, grąžintą po tęsimo.
Tai pakeičia palaikomam poaibiui skirtą PHP Fiber kelią. Fiber pagrįstas vykdiklis (executeGeneratorBody) tebeveikia formoms, kurias kompiliatorius atmeta — yield* delegavimui, generatoriams, kurie pateikia reikšmę try bloke, asinchroniniams generatoriams, asinchroninėms funkcijoms su await. JsGenerator veikia dviem režimais: kiekvienas next() / throw() / return() patikrina, ar egzempliorius turi momentinę kopiją, ar Fiber, ir atitinkamai vykdo.
Nėra našumo bandymams skirto keičiamo parametro; tinkamumas nustatomas kompiliavimo metu ir įsimenamas JsFunction. Momentinių kopijų kelyje kiekvienam pateikimui reikia vienu PHP konteksto perjungimu mažiau; Fiber kelias visa kita apdoroja kaip ir anksčiau.
Įterptiniai podėliai
Op::LOAD_MEMBER kiekvienam PC saugo įterptinį podėlį, kurio raktas yra prototipo tapatybė. Jis naudojamas prototipo metodo egzemplioriuje šablonui (c.sum(...), arr.push(...), kiekvienam klasės metodo iškvietimui). Podėlio atitikties sąlygos: gavėjas neturi nuosavo lizdo šiam pavadinimui, podėlyje išsaugota prototipo nuoroda sutampa su gavėjo getPrototype(), o prototipo PropertyMap::$version vis dar sutampa su užfiksuota versija.
Podėlio anuliavimas vykdomas per PropertyMap::$version. Struktūriniai pakeitimai (nustatymas / trynimas) jį padidina be išimčių. Duomenų lizdo reikšmės perrašymas skaitiklį padidina tik tada, kai PropertyMap::$isUsedAsProto yra true — tai rašymo barjero vėliavėlė, pirmą kartą įjungiama pastebėjus, kad objektas naudojamas kaip kito objekto [[Prototype]]. Egzempliorių objektai, kurie niekada nenaudojami kaip prototipai, už kiekvieną obj.x = v nemoka už įterptinių podėlių infrastruktūrą.
Gavimo prieigos priemonės niekada nepodėliuojamos (jas būtina iš naujo vykdyti kiekvieną kartą skaitant).
PropertyMap turi antrą skaitiklį $mutationVersion, kuris didinamas kiekvieno įrašymo metu (be rašymo barjero). JSON.stringify podėlis kiekvienam peržiūrėtam įdėtam objektui ar masyvui užfiksuoja šį skaitiklį, kad vien egzemplioriuje atliktas pakeitimas, pavyzdžiui, o.a = 99, tinkamai anuliuotų podėliuotą eilutę net tada, kai įterptinio podėlio $version būtų praleidęs skaitiklio padidinimą.
JSON.stringify podėlis
JsObject saugo $jsonCacheString + $jsonCacheVersions ([obj, mutationVersion] porų sąrašą kiekvienam per įprastą eilutės serializavimą peržiūrėtam įdėtam objektui). Kito JSON.stringify(o) iškvietimo metu, jei nėra keitiklio / tarpų, įprastas greitasis kelias patikrina, ar nepasikeitė nė viena užfiksuota versija, ir grąžina podėliuotą eilutę iš naujo neperžiūrėdamas ar nekoduodamas duomenų. Dažnai pasikartojantys gilaus klonavimo šablonai, tokie kaip JSON.parse(JSON.stringify(o)), eilutės serializavimo dalį paspartina maždaug 2 kartus.
Praleidus podėlį, jis tyliai pašalinamas (jei skiriasi kuri nors užfiksuota versija), o kitos peržiūros metu vėl užpildomas. Objektai su toJSON, gavimo prieigos priemonėmis, simboliais ar [[PrimitiveValue]] lizdais perduodami specifikacijos serializatoriui ir niekada nepatenka į podėlį — todėl podėliuotas kelias apdoroja tik struktūriškai vienodus duomenis.
Įtaisytųjų funkcijų spartieji keliai
Didžiausias našumo prieaugis pasiektas išmokius VM tvarkyti konkrečias įtaisytąsias funkcijas:
Atomics.load/store/compareExchangesu kooperatyviu planavimu perloadSpinHook/storeNotifyHook— vėl sėkmingai veikia 4 Atomics sukimosi ciklo bandymai.Date.prototype.getTimezoneOffsetsu tiesiogine maišos lentelės DST podėlio paieška — SpiderMonkey DST apkrovos bandymas 45 % greitesnis.String.fromCharCodesu įterptu ASCII greituoju keliu — per įprastą perėjimą išsaugoma apie 1 M iškvietimų apdorojimų.decodeURI/decodeURIComponentsu 12 simbolių%XX%XX%XX%XXitin sparčiuoju keliu, naudojančiuhex2binmasinį dekodavimą.Object.keys/Object.values/Object.entriessu tiesiogine prieiga prie savybių žemėlapio.Array.prototype.map/filter/forEach/reducesu savuoju iteravimu per pagrindinį PHP masyvą.
Kiekvienas spartusis kelias apsaugotas „formos patikra“ — jei gavėjo forma neatitinka numatytosios (e.g, TypedArray peržiūra, kurios pagrindas nėra įprastas PHP string), VM pereina prie lėtesnio kelio, atitinkančio specifikaciją.
Ko tai nedaro
Baitkodo VM nėra JIT V8 prasme. Ji negeneruoja kiekvienai funkcijai savojo mašininio kodo ir nedaro spekuliacijų pagal pastebėtus tipus, išskyrus monomorfinę prototipo tapatybės patikrą įterptiniame podėlyje. Funkcijų ribų įterpimas apytikriai pasiekiamas tinkinta iškvietimų steko realizacija (kiekvienam JS iškvietimui nėra papildomo PHP kadro), o ne iškviečiamos funkcijos turinį perkeliant į iškvietusios funkcijos baitkodą.
JsToPhp transpiliatorius yra artimiausias variklio JIT atitikmuo: aktyvių funkcijų turiniui jis generuoja PHP kodą, kurį PHP 8.5 sekimo JIT kompiliatorius paverčia savuoju kodu. JsToPhp veikia per eval(), todėl patiria vienkartines kompiliavimo sąnaudas, o sekimo JIT kompiliatorius turi aptikti funkciją taip, lyg ji būtų naudotojo kodas. Dar labiau sumažinti atsilikimą būtų galima praleidus eval() žingsnį ir tiesiogiai generuojant opcache operacijų kodus — žr. Našumo bandymai § Kūrimo planas.
Našumo bandymai
bench/microbench.js vykdo keliolika nedidelių našumo bandymų (loop-arith, loop-fib, fn-recurse, fn-deep-recurse, obj-create, obj-prop, proto-method, gen-iterate, arr-push, arr-map, str-concat, str-split-join, json-roundtrip, closure, destructure). Medianinis sieninis laikas:
php bench/run.php
Naujausi skaičiai po kiekvieno bench darbo eigos vykdymo įrašomi į BENCH.md. bench ir test262 matricų darbo eigos paleidžiamos rankiniu būdu.