Siirry sisältöön

Tavukoodikäyttöympäristö

Oletussuorituspolku on puukävelijä: se vierailee jokaisessa AST-solmussa ja palauttaa arvon. Usein miljoonia kertoja kutsutuissa funktioissa solmukohtainen käsittelykuorma muodostuu pullonkaulaksi. Phasis lisää päälle spekulatiivisen tavukoodista PHP-sulkeumiksi kääntävän kääntäjän tämän kuorman pienentämiseksi.

Milloin VM käynnistyy

Jokaisella JsFunction-funktiolla on usein kutsumisen laskuri. Kun se ylittää kynnyksen (oletusarvo 100), moottori yrittää kääntää funktion rungon src/Bytecode/JsToPhp.php-sulkeumaksi kohteen PHP avulla.

Käännös onnistuu, kun funktion runko sopii tunnettuihin rakenteisiin:

  • Paikallismuuttujien sijoitukset, joiden tyypit ovat ennakoitavissa (numeerinen, olio, funktio, taulukko tai merkkijono).
  • Lukujen aritmetiikka.
  • Jäsenen lukeminen oliotyyppisistä paikallismuuttujista SEKÄ kohteista this (this.v, obj.x).
  • Metodikutsut oliotyyppisistä paikallismuuttujista (obj.method(args)); vastaanottaja välitetään mukana muodossa $thisValue.
  • new Ctor(args)-konstruktorikutsut — paikallismuuttujan LHS:n tyyppi ylennetään muotoon object, ja Bailout-tarkistus varmistaa, ettei palautusarvo ole muu kuin olio.
  • for- ja while-silmukat, joissa on yksinkertaiset ehtorakenteet.
  • return, jonka lauseke voidaan laskea.
  • if / else tyypitetyillä ehdoilla.

Käännös epäonnistuu (ja funktio jää puukävelijälle), jos runko sisältää seuraavia:

  • with-lauseet.
  • try / catch / finally (puukävelijä käsittelee sen oikeiden valmistumissemanttiikkojen mukaisesti).
  • eval()-kutsut.
  • Suora arguments-muutos.
  • Kaikki, mitä kääntäjä ei vielä osaa muuntaa.

Kun käännös onnistuu, moottori vaihtaa funktion kutsupoluksi käännetyn sulkeuman. Seuraavat kutsut suorittavat natiivia PHP-koodia yhden funktiokutsun käsittelykuormalla AST-solmukohtaisen kuorman sijaan.

Keskeytykset

Käännetty nopea polku perustuu oletuksiin (luvut pysyvät lukuina, ominaisuudet säilyvät samanmuotoisina). Jos oletusta rikotaan suorituksen aikana, sulkeuma nostaa Bailout-poikkeuksen ja suoritus aloitetaan uudelleen puukävelijässä kyseistä kutsua varten.

Tässä määrityksen mukaisuus on tärkeintä. Puun läpikäyjän on voitava suorittaa epäonnistumisen aiheuttanut kutsu alusta uudelleen havaitsematta epäonnistuneen nopean polun sivuvaikutuksia. Phasis hoitaa tämän kahdella tavalla:

  1. Puhtaat etuliitteet — kääntäjä etsii sivuvaikutuksia ennen spekulointia. Jos funktio suorittaa I/O-toimintoja (console.log, isäntäfunktiot) ennen spekulointikohtaa, nopea polku ei ohita niitä, vaan sitoutuu ja jatkaa.
  2. Kieltäytyminen käännösaikana — muodoissa, joissa havaittavien sivuvaikutusten peruuttaminen olisi tarpeen (ei-numeerinen let r = fn(), jossa fn() muuttaa tilaa), kääntäjä kieltäytyy kääntämästä lainkaan. Funktio jää puun läpikäyjälle.

Esimerkki: ei-numeerinen sijoitus suorittaa nopealla polulla fn(), aiheuttaa poikkeuksen Bailout ja suoritetaan sitten uudelleen puun läpikäyjällä — jolloin fn() suoritetaan kahdesti ja sivuvaikutuksia syntyy kahdesti. Käännösaikainen kieltäytyminen estää fn()-kutsun suorittamisen nopealla polulla, joten puun läpikäyjä näkee vain yhden kutsun.

VM:n käskyjen suoritus (toinen kerros)

Osa funktioista kääntyy PHP-sulkeumiksi; muut siirtyvät yksinkertaisempaan tavukooditulkkiin src/Bytecode/VM.php. Tavukoodi on litteä joukko käskykoodeja (LOAD_LOCAL, STORE_PROP, CALL_METHOD, …), joita switch-suoritusmekanismi käsittelee.

VM on tarkoitettu funktioille, joita ei kannata kääntää PHP-sulkeumiksi mutta jotka ovat tarpeeksi usein suoritettavia, jotta AST:n läpikäynnin ohittamisesta on hyötyä. Sisäänrakennettujen objektien metodikutsut (Array.prototype.map, String.prototype.split, …) kulkevat VM:n nopeita polkuja pitkin ja ohittavat yleisen callFunction-välittäjän.

Mukautettu kutsupino

Op::CALL, Op::CALL_METHOD ja Op::NEW_CALL käyttävät sisäkkäistä kutsupolkua, joka pitää koko JS-kutsupinon VM::execute:ssä — jokaista JS-kutsua varten ei luoda uutta PHP-kehystä. Kutsun alussa kutsujan tila (pc, koodivirta, operandipino, paikalliset muuttujat, env, this, strict-tilan lippu) tallennetaan VM-kohtaiseen savedFrames-pinoon, johon sijoitetaan SavedFrame-instansseja, ja suorituslooppi päivittää paikalliset muuttujansa kutsuttavan funktion mukaan. Op::RET poistaa tallennetun kehyksen pinosta ja palauttaa sen. Poikkeukset purkautuvat tallennettujen kehysten läpi ennen paluuta puun läpikäyjälle.

Soveltuvuus tallennetaan välimuistiin kohteessa JsFunction: vmInlineCallCache tavallisille kutsuille (BC-käännetty runko, ei bind()-sidottu, ei natiivi), vmInlineConstructCache kohteelle new-lausekkeet (lisäksi isConstructable, ei johdettu, ei instanssikenttä- tai yksityisen metodin alustajia). Kaikki muu kulkee yhä kanonisen callFunction / vmNewExpression-välittäjän kautta.

Arkkitehtuurin sivuhyöty: PHP-pinon yläraja ei enää rajoita JS-rekursion syvyyttä. Nyt ainoa raja on CallStack::maxDepth — arvona 4 096, mikä ylittää reilusti kaikkien kelvollisten test262-tapausten tarpeet (useimmat tarvitsevat alle 2 k kehystä) ja pitää arvon riittävän pienenä, jotta patologiset äärettömän rekursion testit epäonnistuvat nopeasti suorittajan aikakatkaisun puitteissa. Transpiloidun sulkeuman polulla (JsToPhp) on oma 8 192-kehyksen rajoitin sisäkkäistämättömälle varapolulle.

Kehysvedoksiin perustuvat generaattorit

Yksinkertaiset generaattorien rungot — ei yield*, ei yield try/catch/finally-lohkossa, ei await — käännetään tavukoodiksi. Kääntäjä tuottaa uuden Op::YIELD-käskykoodin kohtaan, jossa runko keskeytyy; Op::YIELD tallentaa suorittimen tilan (cf, pc+1, pino, sp, paikalliset muuttujat, env, thisValue, strict) GeneratorSnapshot-keon objektiin ja palauttaa YieldResult-sentinelin kohteesta VM::execute. Ohjaava JsGenerator tallentaa vedoksen ja syöttää sen takaisin kohteelle VM::execute seuraavan next() / throw()-kutsun yhteydessä korvaten operandipinon huipulla olevan jatkoarvon.

Tämä korvaa tuetussa osajoukossa PHP-Fiber-polun. Fiber-pohjainen ohjain (executeGeneratorBody) on yhä käytössä muodoille, jotka kääntäjä hylkää — yield*-delegointi, generaattorit, jotka tuottavat arvoja try-lohkon sisällä, asynkroniset generaattorit sekä asynkroniset funktiot, joissa on await. JsGenerator toimii kahdessa tilassa: jokainen next() / throw() / return() tarkistaa, onko instanssilla vedos vai Fiber, ja suorittaa sen mukaisen polun.

Säädettävää suorituskykyasetusta ei ole; soveltuvuus ratkaistaan käännösaikana ja tallennetaan välimuistiin kohteessa JsFunction. Vedospolku säästää yhden PHP-kontekstinvaihdon jokaista arvon tuottoa kohti; Fiber-polku toimii ennallaan kaikessa muussa.

Sisäiset välimuistit

Op::LOAD_MEMBER sisältää ohjelmalaskurikohtaisen sisäisen välimuistin, jonka avaimena on prototyypin identiteetti. Sitä käytetään tilanteissa, joissa prototyypin metodia kutsutaan instanssilla (c.sum(...), arr.push(...) ja jokaisen luokan metodin suoritus). Osuman ehdot: vastaanottajalla ei ole omaa paikkakenttää nimelle, välimuistiin tallennettu prototyyppiviite on identtinen vastaanottajan getPrototype()-viitteen kanssa ja prototyypin PropertyMap::$version vastaa yhä tallennettua versiota.

Välimuistin mitätöinti perustuu kohteeseen PropertyMap::$version. Rakenteelliset muutokset (asetus / poisto) kasvattavat sen arvoa aina. Data-paikkakentän arvon korvaaminen kasvattaa sitä vain, kun PropertyMap::$isUsedAsProto on tosi — kyseessä on kirjoituksenvalvontalippu, joka asetetaan ensimmäisellä kerralla, kun objekti havaitaan toisen objektin [[Prototype]]-ominaisuutena. Instanssiobjekteille, jotka eivät koskaan toimi prototyyppeinä, välimuisti-infrastruktuurista ei koidu lisäkustannuksia jokaisella obj.x = v-kerralla.

Getter-ominaisuuksia ei koskaan tallenneta välimuistiin (ne on suoritettava uudelleen jokaisella lukukerralla).

PropertyMap sisältää toisen laskurin, $mutationVersion, jonka arvoa kasvatetaan aina jokaisen kirjoituksen yhteydessä (ilman kirjoituksenvalvontaa). JSON.stringify-välimuisti kiinnittää tämän laskurin jokaiseen läpikäytyyn sisäkkäiseen objektiin/taulukkoon, jotta pelkkään instanssiin kohdistuva muutos, kuten o.a = 99, mitätöi välimuistissa olevan merkkijonon oikein silloinkin, kun IC:n $version olisi muuten ohittanut laskurin kasvattamisen.

JSON.stringify-välimuisti

JsObject sisältää kohteet $jsonCacheString + $jsonCacheVersions (luettelon [obj, mutationVersion]-pareista jokaiselle triviaalin merkkijonoksi muunnon aikana läpikäydylle sisäkkäiselle objektille). Seuraavalla JSON.stringify(o)-kutsulla ilman korvaajaa tai välilyöntiä triviaali nopea polku tarkistaa, ettei minkään kiinnitetyn version arvo ole muuttunut, ja palauttaa välimuistissa olevan merkkijonon käymättä rakennetta uudelleen läpi tai koodaamatta sitä uudelleen. Usein käytetyt syvän kopioinnin mallit, kuten JSON.parse(JSON.stringify(o)), nopeuttavat merkkijonoksi muunnon puoliskoa noin kaksinkertaisesti.

Välimuisti tyhjennetään hiljaisesti osuman puuttuessa (kun jonkin kiinnitetyn version arvo eroaa) ja täytetään seuraavalla läpikäynnillä. Objektit, joissa on toJSON, gettereitä, asettajia, symboleja tai [[PrimitiveValue]]-paikkakenttiä, siirtyvät määrityksen mukaiseen sarjallistajaan eivätkä koskaan täytä välimuistia — näin välimuistipolulla käsitellään vain rakenteeltaan yhdenmukaista dataa.

Sisäänrakennettujen toimintojen nopeat polut

Suurimmat suorituskykyparannukset saatiin opettamalla VM tuntemaan tietyt sisäänrakennetut toiminnot:

  • Atomics.load / store / compareExchange ja loadSpinHook / storeNotifyHook-yhteistoiminnallinen ajoitus — palautti käyttöön 4 Atomics-spin-loop-testiä.
  • Date.prototype.getTimezoneOffset suoralla hajautustaulun DST-välimuistilla — 45 % nopeutus SpiderMonkey-DST-rasitustestissä.
  • String.fromCharCode sisäkkäisellä ASCII-nopealla polulla — säästää noin 1 M suoritusaskelta tyypillistä läpikäyntiä kohti.
  • decodeURI / decodeURIComponent 12 merkin %XX%XX%XX%XX-ultranopealla polulla, joka käyttää kohteen hex2bin massapurkua.
  • Object.keys / Object.values / Object.entries suoralla ominaisuuskarttaan pääsyllä.
  • Array.prototype.map / filter / forEach / reduce alla olevan PHP-taulukon natiivilla läpikäynnillä.

Jokainen nopea polku on rajattu "rakennetarkistuksella" — jos vastaanottaja ei vastaa odotettua rakennetta (e.g. TypedArray-näkymä, joka ei perustu tavalliseen PHP string:ään), VM siirtyy määrityksen mukaiseen hitaaseen polkuun.

Mitä tämä ei tee

Tavukoodi-VM ei ole JIT merkityksessä V8. Se ei tuota natiivia konekoodia funktiokohtaisesti eikä spekuloi havaittujen tyyppien perusteella IC:n monomorfista prototyypin identiteettitarkistusta pidemmälle. Funktiorajojen sisäkkäistä kutsua jäljitellään mukautetulla kutsupinolla (jokaiselle JS-kutsulle ei luoda ylimääräistä PHP-kehystä), eikä kääntämällä kutsuttavan funktion runkoa kutsujan tavukoodiksi.

JsToPhp-transpilointiohjelma on moottorin JIT:iä lähinnä oleva osa: se tuottaa PHP-lähdekoodia usein suoritettaville funktiorungoille, jonka PHP 8.5:n jäljittävä JIT kääntää sitten natiiviksi koodiksi. JsToPhp kulkee kohteen eval() kautta, joten siitä aiheutuu kertaluonteinen käännöskustannus ja jäljittävän JIT:n on löydettävä funktio aivan kuin se olisi käyttäjän koodia. Eroa saataisiin pienennettyä ohittamalla eval()-vaihe ja tuottamalla opcache-käskyt suoraan — katso Vertailutestit § Tiekartta.

Vertailutesti

bench/microbench.js suorittaa runsaat tusinan verran pieniä vertailutestejä (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). Ajoajan mediaani:

Shell
php bench/run.php

Nykyiset tulokset tallennetaan kohteeseen BENCH.md jokaisen bench-työnkulkusuorituksen jälkeen. Työnkulut bench ja test262-matriisi käynnistetään kumpikin käsin.