Baitu koda virtuālā mašīna
Noklusējuma izpildes ceļš ir koka apstaigātājs — tiek apmeklēts katrs AST mezgls un atgriezta vērtība. Bieži izsauktām funkcijām, kas tiek palaistas miljoniem reižu, par sašaurinājumu kļūst katra mezgla apstrādes pieskaitāmās izmaksas. Phasis virsū pievieno spekulatīvu baitu koda uz PHP slēgumu kompilatoru, lai tās samazinātu.
Kad ieslēdzas virtuālā mašīna
Katram JsFunction ir biežo izsaukumu skaitītājs. Kad tas pārsniedz slieksni (noklusējuma vērtība 100), dzinējs mēģina kompilēt funkcijas pamatkodu par PHP slēgumu, izmantojot src/Bytecode/JsToPhp.php.
Kompilēšana izdodas, ja funkcijas pamatkods atbilst kādai no zināmajām formām:
- Lokālo mainīgo piešķīrumi ar paredzamiem tipiem (skaitlis, objekts, funkcija, masīvs, virkne).
- Aritmētiskas darbības ar skaitļiem.
- Īpašību nolasīšana objektu tipa lokālajiem mainīgajiem UN
this(this.v,obj.x). - Metožu izsaukumi objektu tipa lokālajiem mainīgajiem (
obj.method(args)), saņēmēju padodot kā$thisValue. new Ctor(args)konstruktora izsaukumi — LHS lokālajam mainīgajam piešķirobjecttipu un izmanto Bailout pārbaudi, ja atgrieztā vērtība nav objekts.for/whilecikli ar vienkāršiem nosacījumiem.returnar aprēķināmu izteiksmi.if/else, kuru nosacījumi ir tipizēti.
Kompilēšana neizdodas (un funkcija paliek koka apstaigātājā), ja pamatkodā ir:
withpriekšraksti.try / catch / finally(koka apstaigātājs to apstrādā, lai nodrošinātu pareizu pabeigšanas semantiku).eval()izsaukumi.- Tieša
argumentsmutācija. - Jebkas, ko kompilators vēl neprot pārveidot.
Ja kompilēšana izdodas, dzinējs nomaina funkcijas izsaukuma ceļu uz kompilēto slēgumu. Turpmākajos izsaukumos tiek izpildīts vietējais PHP ar vienām funkcijas izsaukuma izmaksām, nevis atsevišķi katram AST mezglam.
Atkāpšanās no ātrā ceļa
Kompilētais ātrais ceļš balstās uz pieņēmumiem (skaitļi paliek skaitļi, īpašības paliek tajā pašā formā). Ja izpildes laikā kāds pieņēmums izrādās aplams, slēgums izmet Bailout, un attiecīgais izsaukums tiek sākts no jauna koka apstaigātājā.
Šeit visvairāk svarīga ir atbilstība specifikācijai. Koka apstaigātājam jāspēj atkārtoti izpildīt no jauna izsaukumu, kurā notika atkāpšanās, nemanot neveiksmīgā ātrā ceļa blaknes. Phasis to nodrošina divos veidos:
- Tīri prefiksi — kompilators pirms jebkādas spekulācijas pārbauda blaknes. Ja funkcija pirms spekulācijas punkta veic ievadi vai izvadi (console.log, resursdatora funkcijas), ātrais ceļš tās neizlaiž — tas izpilda tās un turpina.
- Atteikšanās kompilēšanas laikā — ja formai būtu jāatceļ novērojamas blaknes (piemēram,
let r = fn()nav skaitlisks, betfn()maina stāvokli), kompilators atsakās to kompilēt. Funkcija paliek koka apstaigātājā.
Motivējošais gadījums: nepiešķiršana skaitliskai vērtībai ātrajā izpildes ceļā izpilda fn(), izmet Bailout un pēc tam tiek izpildīta atkārtoti koka interpretatorā — fn() darbojas divreiz, radot divas blakņu kopas. Atteikums kompilēšanas laikā neļauj fn() vispār darboties ātrajā izpildes ceļā, tāpēc koka interpretators redz vienīgo izsaukumu.
VM dispečēšana (otrais slānis)
Dažas funkcijas tiek kompilētas par PHP slēgumiem; citas nonāk vēl vienkāršākā baitkoda interpretatorā, kas darbojas src/Bytecode/VM.php. Baitkods ir plakans opkodu masīvs (LOAD_LOCAL, STORE_PROP, CALL_METHOD, …), ko apstrādā switch dispečēšana.
VM paredzēta funkcijām, kuras nav vērts kompilēt par PHP slēgumu, bet kuras joprojām tiek izsauktas pietiekami bieži, lai iegūtu labumu no AST šķērsošanas izlaišanas. Metožu izsaukumi iebūvētiem objektiem (Array.prototype.map, String.prototype.split, …) VM izmanto ātrās izpildes ceļus, kas apiet vispārīgo callFunction tramplīnu.
Pielāgota izsaukumu steka pārvaldība
Op::CALL, Op::CALL_METHOD un Op::NEW_CALL izmanto iekļautu izsaukumu ceļu, kas visu JS izsaukumu steku uztur VM::execute — katram JS izsaukumam netiek veidots jauns PHP kadrs. Ieejot funkcijā, izsaucēja stāvoklis (pc, koda plūsma, operandu steks, lokālie mainīgie, vide, this, stingrā režīma karodziņš) tiek ievietots katrai VM atsevišķā savedFrames pūlā, kurā ir SavedFrame instances, un dispečēšanas cikls pārveido savus lokālos mainīgos, lai tie norādītu uz izsaucamo funkciju. Op::RET izņem saglabāto kadru un atjauno stāvokli. Izņēmumi tiek atritināti pāri saglabātajiem kadriem, pirms kontrole tiek nodota koka interpretatoram.
Atbilstība tiek saglabāta kešā JsFunction: vmInlineCallCache parastiem izsaukumiem (ar baitkodā kompilētu pamatdaļu, nav saistīts ar bind(), nav iebūvēts), vmInlineConstructCache par new izteiksmēm (papildus isConstructable, nav atvasināts, nav instanču lauku vai privāto metožu inicializatoru). Viss pārējais joprojām tiek apstrādāts caur kanonisko callFunction / vmNewExpression tramplīnu.
Arhitektūras papildu ieguvums: PHP steka ierobežojums vairs neierobežo JS rekursijas dziļumu. Tagad vienīgais ierobežojums ir CallStack::maxDepth — iestatīts uz 4 096, kas ir krietni vairāk par visām leģitīmajām test262 vajadzībām (vairumam pietiek ar mazāk nekā 2 k kadriem) un pietiekami stingrs, lai patoloģiski bezgalīgas rekursijas testi ātri neizturētu izpildlaika ierobežojumu. Transpilēto slēgumu ceļam (JsToPhp) ir savs 8 192 kadru ierobežojums neiekļautajam rezerves ceļam.
Ģeneratori ar kadra momentuzņēmumu
Vienkāršu ģeneratoru pamatdaļas — bez yield*, bez yield try/catch/finally blokā, bez await — tiek kompilētas baitkodā. Kompilators ģenerē jaunu Op::YIELD opkodu vietā, kur pamatdaļas izpilde tiek apturēta; Op::YIELD tver dispečēšanas stāvokli (cf, pc+1, steks, sp, lokālie mainīgie, vide, thisValue, strict) GeneratorSnapshot kaudzes objektā un atgriež YieldResult marķieri no VM::execute. Pārvaldošais JsGenerator saglabā momentuzņēmumu un nākamajā next() / throw() reizē nodod to atpakaļ VM::execute, aizstājot operandu steka augšdaļā esošo atsākšanas vērtību.
Atbalstītajā gadījumu apakškopā tas aizstāj PHP Fiber ceļu. Uz Fiber balstītais pārvaldītājs (executeGeneratorBody) joprojām tiek izmantots gadījumiem, ko kompilators noraida — yield* delegācijai, ģeneratoriem, kas padod vērtību try blokā, asinhronajiem ģeneratoriem un asinhronajām funkcijām ar await. JsGenerator darbojas divos režīmos: katrs next() / throw() / return() pārbauda, vai instancei ir momentuzņēmums vai Fiber, un attiecīgi izvēlas dispečēšanas ceļu.
Nav ar etalona testiem pielāgojamas opcijas; atbilstība tiek noteikta kompilēšanas laikā un saglabāta kešā JsFunction. Ieguvums ir par vienu mazāk PHP konteksta pārslēgšanos katrā vērtības padošanas reizē momentuzņēmuma ceļā; Fiber ceļš pārējiem gadījumiem paliek nemainīgs.
Iekļautās kešatmiņas
Op::LOAD_MEMBER katram PC uztur iekļauto kešatmiņu, kuras atslēga ir prototipa identitāte; tā tiek izmantota prototipa metodes izsaukšanai instancē (c.sum(...), arr.push(...), visu klašu metožu dispečēšana). Kešatmiņas trāpījuma nosacījumi: saņēmējam nav savas nosaukumam atbilstošas vietas, kešotā atsauce uz prototipu ir identiska saņēmēja getPrototype(), un prototipa PropertyMap::$version joprojām atbilst fiksētajai versijai.
Kešatmiņas nederīguma noteikšana balstās uz PropertyMap::$version. Strukturālas izmaiņas (set / delete) to palielina bez nosacījumiem. Datu vietas vērtības pārrakstīšana to palielina tikai tad, ja PropertyMap::$isUsedAsProto ir true — tas ir rakstīšanas barjeras karodziņš, kas tiek ieslēgts, kad objekts pirmoreiz tiek novērots kā cita objekta [[Prototype]]. Instanču objektiem, kas nekad nekalpo par prototipiem, katrā obj.x = v nav jāmaksā par iekļautās kešatmiņas infrastruktūru.
Getter piekļuves metodes nekad netiek kešotas (tās jāizpilda atkārtoti katrā nolasīšanas reizē).
PropertyMap uztur otru skaitītāju — $mutationVersion — kas pieaug bez nosacījumiem katrā rakstīšanas reizē (bez rakstīšanas barjeras). JSON.stringify kešatmiņa piesaista šo skaitītāju katram apmeklētajam ligzdotajam objektam vai masīvam, lai, piemēram, izmaiņas tikai instancē kā o.a = 99 pareizi padarītu kešoto virkni nederīgu, pat ja iekļautās kešatmiņas $version būtu izlaidis skaitītāja palielināšanu.
JSON.stringify kešatmiņa
JsObject uztur $jsonCacheString + $jsonCacheVersions ([obj, mutationVersion] pāru sarakstu katram ligzdotajam objektam, kas apmeklēts vienkāršās stringify apstrādes laikā). Nākamajā JSON.stringify(o) izsaukumā bez aizvietotāja vai atstarpēm vienkāršais ātrais ceļš pārbauda, vai visu piesaistīto versiju vērtības nav mainījušās, un atgriež kešoto virkni, neveicot atkārtotu caurskati vai kodēšanu. Bieži izmantotie dziļās klonēšanas paraugi, piemēram, JSON.parse(JSON.stringify(o)), stringify posmā ir aptuveni 2× ātrāki.
Kešatmiņa tiek nemanāmi atmesta, ja tajā neatrod trāpījumu (jebkura piesaistītā versija atšķiras), un aizpildīta nākamās caurskates laikā. Objekti ar toJSON, getteriem, piekļuves metodēm, simboliem vai [[PrimitiveValue]] vietām pāriet uz specifikācijā noteikto serializētāju un nekad neaizpilda kešatmiņu — kešotais ceļš redz tikai strukturāli vienveidīgus datus.
Iebūvēto funkciju ātrie ceļi
Lielākie veiktspējas ieguvumi radās, kad VM tika pielāgota konkrētām iebūvētajām funkcijām:
Atomics.load/store/compareExchangearloadSpinHook/storeNotifyHookkooperatīvo plānošanu — atjaunota 4 Atomics griešanās ciklu testu darbība.Date.prototype.getTimezoneOffsetar tiešās heškartes DST kešatmiņu — par 45 % ātrāki SpiderMonkey DST stresa testi.String.fromCharCodear iekļautu ASCII ātro ceļu — tipiskā pārbaudē tiek ietaupīts ~1 M dispečēšanas reižu.decodeURI/decodeURIComponentar 12 rakstzīmju%XX%XX%XX%XXīpaši ātro ceļu, kas izmantohex2binpakešveida dekodēšanu.Object.keys/Object.values/Object.entriesar tiešu piekļuvi īpašību kartēm.Array.prototype.map/filter/forEach/reducear vietējām iterācijām pamatā esošajā PHP masīvā.
Katru ātro ceļu aizsargā „formas pārbaude” — ja saņēmējs neatbilst gaidītajai formai (e.g, piemēram, TypedArray skats, kura pamatā nav parasts PHP string), VM pāriet uz lēno ceļu, kas atbilst specifikācijai.
Ko tas nedara
Baitkoda VM nav JIT V8 izpratnē. Tā katrai funkcijai neģenerē vietējo mašīnkodu un nespekulē, balstoties uz novērotajiem tipiem, izņemot monomorfo prototipa identitātes pārbaudi iekļautajā kešatmiņā. Funkciju robežu iekļaušana tiek tuvināta ar pielāgoto izsaukumu steku (bez papildu PHP kadra katram JS izsaukumam), nevis izsaucamās funkcijas pamatdaļu pārveidojot par izsaucēja baitkodu.
JsToPhp transpilētājs ir dzinēja daļa, kas visvairāk līdzinās JIT: tas ģenerē PHP pirmkodu bieži izpildāmu funkciju pamatdaļām, ko PHP 8.5 izsekošanas JIT pēc tam kompilē vietējā kodā. JsToPhp izmanto eval(), tāpēc tas maksā vienreizēju kompilēšanas cenu, un izsekošanas JIT funkcija jāatklāj tā, it kā tā būtu lietotāja kods. Izlaižot eval() soli un tieši ģenerējot opcache opkodus, atšķirība samazinātos vēl vairāk — skatiet Ceļveža sadaļu “Etalona testi”.
Etalona testi
bench/microbench.js izpilda apmēram duci nelielu etalontestu (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). Mediānais izpildes laiks:
php bench/run.php
Pašreizējie skaitļi pēc katras bench darbplūsmas izpildes tiek pievienoti BENCH.md. Gan bench, gan test262 matricu darbplūsmas tiek palaistas manuāli.