Liigu sisu juurde

Baidikoodi virtuaalmasin

Vaikimisi käib täitmine puupõhise interpretaatori kaudu — külastatakse iga AST-sõlme ja tagastatakse väärtus. Miljoneid kordi kutsutavate kuumade funktsioonide puhul on pudelikaelaks iga sõlme väljakutse lisakulu. Phasis lisab selle vähendamiseks spekulatiivse baidikoodist PHP sulgude kompilaatori.

Millal virtuaalmasin käivitub

Igal JsFunction on kuumade kutsete loendur. Kui see ületab lävendi (vaikimisi 100), proovib mootor kompileerida funktsiooni keha src/Bytecode/JsToPhp.php kaudu PHP suluks.

Kompileerimine õnnestub, kui funktsiooni keha sobib teadaolevate kujude hulka:

  • Lokaalmuutujate omistamised ennustatavate tüüpidega (arv, objekt, funktsioon, massiiv, string).
  • Aritmeetika arvudega.
  • Liikmete lugemine objektitüüpi lokaalmuutujatelt JA this-ilt (this.v, obj.x).
  • Meetodite kutsumine objektitüüpi lokaalmuutujatel (obj.method(args)), kus vastuvõtja edastatakse kui $thisValue.
  • new Ctor(args) konstruktorikutsed — muudavad LHS-lokaalmuutuja tüübi väärtuseks object ja lisavad loobumiskontrolli, kui tagastusväärtus pole objekt.
  • for / while tsüklid lihtsate tingimustega.
  • return arvutatava avaldisega.
  • if / else tüübistatud tingimustel.

Kompileerimine ebaõnnestub (ja funktsioon jääb puupõhisesse interpretaatorisse), kui keha sisaldab:

  • with lauseid.
  • try / catch / finally (puupõhine interpretaator käsitleb seda lõpetamise semantika tagamiseks).
  • eval() kutseid.
  • Otsest arguments muutmist.
  • Midagi, mida kompilaator veel madalamale tasemele teisendada ei oska.

Kui kompileerimine õnnestub, vahetab mootor funktsiooni kutsetee kompileeritud sulu vastu. Järgmised kutsed käivitavad natiivset PHP-i ühe funktsioonikutse lisakuluga, mitte iga AST-sõlme kohta eraldi.

Loobumised

Kompileeritud kiirtee eeldab teatud tingimusi (arvud jäävad arvudeks, atribuudid jäävad samasse kujusse). Kui käitusajal mõni eeldus ei kehti, viskab sulg välja Bailout ning täitmine jätkub selle kutse puhul puupõhises interpretaatoris.

Siin on spetsifikatsioonile vastavus kõige olulisem. Puupõhine interpretaator peab saama loobunud kutse algusest peale uuesti käivitada, ilma et ebaõnnestunud kiirtee kõrvalmõjud oleks nähtavad. Phasis lahendab selle kahel viisil:

  1. Puhtad eesliited — kompilaator otsib enne spekuleerimist kõrvalmõjusid. Kui funktsioon teeb enne spekuleerimispunkti sisend-väljundtoiminguid (console.log, host-funktsioonid), ei jäta kiirtee neid vahele, vaid viib need lõpuni.
  2. Kompileerimisest keeldumine — kujude puhul, mis nõuaksid nähtavate kõrvalmõjude tagasipööramist (mittenumbriline let r = fn(), mille korral fn() muudab olekut), keeldub kompilaator kompileerimast. Funktsioon jääb puupõhisesse interpretaatorisse.

Näide, mis seda ajendas: mittenumbriline omistamine käivitab kiirteel fn(), viskab välja Bailout ja käivitub seejärel puupõhises interpretaatoris uuesti — fn() käivitatakse kaks korda ja mõlemal korral tekivad kõrvalmõjud. Kompileerimisest keeldumine hoiab ära fn() käivitamise kiirteel, nii et puupõhine interpretaator näeb ainult üht kutset.

Virtuaalmasina väljakutse (teine kiht)

Osa funktsioone kompileeritakse PHP sulgudeks, teised kasutavad src/Bytecode/VM.php-is veelgi lihtsamat baidikoodi interpretaatorit. Baidikood on lame opkoodide massiiv (LOAD_LOCAL, STORE_PROP, CALL_METHOD, …), mida töötleb switch väljakutse.

Virtuaalmasin on mõeldud funktsioonidele, mida pole mõtet PHP suluks kompileerida, kuid mis on siiski piisavalt sageli käivitatavad, et AST läbimise vahelejätmisest kasu saada. Sisseehitatud objektide meetodikutseid (Array.prototype.map, String.prototype.split, …) töötlevad virtuaalmasina kiirteed, mis jätavad üldise callFunction vahenduskihi vahele.

Kohandatud kutsepinu

Op::CALL, Op::CALL_METHOD ja Op::NEW_CALL kasutavad inline-kutse teed, mis hoiab kogu JS kutsepinu VM::execute sees — iga JS kutse jaoks ei looda uut PHP raami. Sisenemisel salvestatakse kutsuja olek (pc, koodivoog, operandipinu, lokaalid, env, this, range režiimi lipp) VM-ile omasesse savedFrames kogumisse, mis sisaldab SavedFrame eksemplare, ning väljakutsesilmus ühendab lokaalid kutsutava funktsiooniga. Op::RET eemaldab salvestatud raami ja taastab oleku. Erandid keritakse läbi salvestatud raamide enne puupõhise interpretaatori juurde naasmist.

Sobivus jäetakse meelde üksusel JsFunction: vmInlineCallCache tavaliste kutsete puhul (BC-kompileeritud keha, pole seotud bind()-iga ega natiivne), vmInlineConstructCache new-avaldiste puhul (lisaks isConstructable, pole tuletatud klass, pole eksemplariväljade ega privaatmeetodite algväärtustajaid). Kõik muu läbib endiselt kanoonilise callFunction / vmNewExpression vahenduskihi.

Arhitektuurne kõrvalkasu: PHP-i pinu ülempiir ei piira enam JS rekursiooni sügavust. Nüüd on ainsaks piiriks CallStack::maxDepth — väärtuseks on määratud 4 096, mis ületab mugavalt kõigi õiguspäraste test262 juhtude vajadused (enamik vajab alla 2 k kaadri) ja on piisavalt väike, et patoloogilise lõpmatu rekursiooni testid ebaõnnestuksid käitaja ajalimiidi piires kiiresti. Transpileeritud sulgude tee (JsToPhp) kasutab mittesisese fallback-tee jaoks eraldi 8 192-kaadri piirangut.

Kaadripildi generaatorid

Lihtsad generaatorikehad — puudub yield*, try/catch/finally sees puudub yield ning puudub await — kompileeritakse baidikoodiks. Kompilaator väljastab uue Op::YIELD opkoodi kohta, kus keha peatub; Op::YIELD jäädvustab väljakutse oleku (cf, pc+1, pinu, sp, lokaalid, env, thisValue, strict) GeneratorSnapshot kuhjaobjekti ja tagastab VM::execute-ist YieldResult kohatäitja. Käitav JsGenerator salvestab kaadripildi ja edastab selle järgmisel next() / throw() korral tagasi funktsioonile VM::execute, asendades operandipinu tipus oleva jätkamisväärtuse.

Toetatud alamhulga puhul asendab see PHP-Fiberi tee. Fiber-põhine käitaja (executeGeneratorBody) jääb kasutusse kujude jaoks, mille kompilaator tagasi lükkab — yield* delegeerimine, generaatorid, mis annavad väärtuse try-ploki sees, asünkroonsed generaatorid ning asünkroonsed funktsioonid koos await-iga. JsGenerator töötab kahes režiimis: iga next() / throw() / return() kontrollib, kas eksemplaril on kaadripilt või Fiber, ja suunab täitmise vastavalt.

Seadistusnuppu pole vaja; sobivus otsustatakse kompileerimise ajal ja jäetakse meelde üksusel JsFunction. Kaadripildi teel jääb iga yield'i kohta ära üks PHP kontekstivahetus; muu puhul jääb Fiberi tee samaks.

Inline-vahemälud

Op::LOAD_MEMBER-il on iga PC kohta prototüübi identiteediga võtmetud inline-vahemälu, mida kasutatakse prototüübi meetodi kutsumisel eksemplaril (c.sum(...), arr.push(...) ja iga klassimeetodi väljakutse). Vahemälu tabab, kui vastuvõtjal pole sellele nimele oma välja, vahemällu salvestatud prototüübiviide on sama mis vastuvõtja getPrototype() ning prototüübi PropertyMap::$version vastab endiselt jäädvustatud versioonile.

Vahemälu kehtetuks muutmine kasutab PropertyMap::$version-i. Struktuurimuudatused (seadmine / kustutamine) suurendavad seda tingimusteta. Andmevälja väärtuse ülekirjutamine suurendab seda ainult siis, kui PropertyMap::$isUsedAsProto on tõene — kirjutuspiirde lipp lülitatakse sisse esimest korda, kui objekti vaadeldakse teise objekti [[Prototype]]-ina. Eksemplariobjektid, mida ei kasutata kunagi prototüüpidena, ei kanna IC-taristu kulu iga obj.x = v puhul.

Getter-juurdepääsufunktsioone ei salvestata kunagi vahemällu (neid tuleb iga lugemise korral uuesti käivitada).

PropertyMap-il on teine loendur, $mutationVersion, mis suureneb tingimusteta iga kirjutamise korral (kirjutuspiiret pole). JSON.stringify vahemälu fikseerib selle loenduri iga külastatud pesastatud objekti/massiivi puhul, et ainult eksemplari muutmine, näiteks o.a = 99, muudaks vahemällu salvestatud stringi kehtetuks ka siis, kui IC $version oleks loenduri suurendamise vahele jätnud.

JSON.stringify vahemälu

JsObject-il on $jsonCacheString + $jsonCacheVersions (loend [obj, mutationVersion] paaridest iga triviaalset stringify-käiku läbinud pesastatud objekti kohta). Järgmisel JSON.stringify(o) kutsel ilma asendaja ja tühikuta kontrollib triviaalne kiirtee, et kõigi fikseeritud versioonid on muutumatud, ning tagastab vahemällu salvestatud stringi ilma uuesti läbimise või kodeerimiseta. Kuumad süvakloonimise mustrid nagu JSON.parse(JSON.stringify(o)) kiirendavad stringifitseerimise osa ligikaudu 2×.

Vahemälu tühjendatakse vaikselt möödalasu korral (kui fikseeritud versioon erineb) ja täidetakse uuesti järgmisel läbimisel. Objektid, millel on toJSON, getterid, juurdepääsufunktsioonid, sümbolid või [[PrimitiveValue]] väljad, suunatakse spetsifikatsioonikohasesse serialiseerijasse ega täida kunagi vahemälu — seega töötleb vahemällu salvestatud tee ainult ühtlase struktuuriga andmeid.

Sisseehitatud kiirteed

Suurim jõudluse kasv saadi virtuaalmasinat konkreetsete sisseehitatud funktsioonide osas täiustades:

  • Atomics.load / store / compareExchange koos loadSpinHook / storeNotifyHook koostööpõhise ajastamisega — taastati 4 Atomicsi pöördtsükli testi.
  • Date.prototype.getTimezoneOffset otsese räsitabeli DST-vahemäluga — 45 % kiirem SpiderMonkey DST koormustestis.
  • String.fromCharCode ASCII inline-kiirteega — säästab tüüpilise läbimise kohta ~1 M väljakutset.
  • decodeURI / decodeURIComponent 12 märgi %XX%XX%XX%XX ülikiire teega, mis kasutab hex2bin massdekodeerimist.
  • Object.keys / Object.values / Object.entries otsese atribuudikaardi juurdepääsuga.
  • Array.prototype.map / filter / forEach / reduce aluseks oleva PHP massiivi natiivse iteratsiooniga.

Iga kiirtee algab „kujukontrolliga“ — kui vastuvõtja ei vasta eeldatud kujule (e.g, näiteks TypedArray vaade, mis ei põhine lihtsal PHP string-il), jätkab virtuaalmasin spetsifikatsioonikohase aeglase teega.

Mida see ei tee

Baidikoodi virtuaalmasin ei ole JIT V8 tähenduses. See ei genereeri iga funktsiooni jaoks natiivset masinkoodi ega spekuleeri vaadeldud tüüpide põhjal kaugemale kui IC monomorfne prototüübi-identiteedi kontroll. Funktsioonipiiri sisestamist jäljendab kohandatud kutsepinu (iga JS kutse kohta pole lisanduvat PHP raami), mitte kutsutava funktsiooni keha teisendamine kutsuja baidikoodiks.

JsToPhp transpilaator on mootoris JIT-ile kõige lähemal: see genereerib kuumade funktsioonide kehade jaoks PHP lähtekoodi, mille PHP 8.5 jälgiv JIT seejärel natiivkoodiks kompileerib. JsToPhp kasutab eval()-i, mistõttu kaasneb ühekordne kompileerimiskulu ja jälgiv JIT peab funktsiooni avastama nagu kasutajakoodi. eval() sammu vahelejätmine otse opcache-opkoodide genereerimisega vähendaks vahet veelgi — vt Võrdlustestid § Teekaart.

Võrdlustestid

bench/microbench.js käivitab kümmekond pisikest võrdlustesti ja veel mõned (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). Seinaaja mediaan:

Shell
php bench/run.php

Praegused näitajad salvestatakse faili BENCH.md pärast iga bench töövoo käivitamist. Nii bench kui ka test262 maatriksi töövood käivitatakse käsitsi.