Hur JavaScript fungerar: i V8-motor + 5 tips på hur du kan skriva optimerad kod

Aug 21, 2017 · 11 min läsa

För några veckor sedan som vi startade en serie som syftar till att gräva djupare i JavaScript och hur det faktiskt fungerar: vi trodde att genom att veta de byggstenar av JavaScript och hur de kommer att spela tillsammans kommer du att kunna skriva bättre kod och appar.,

det första inlägget i serien fokuserade på att ge en översikt över motorn, körtiden och samtalsstacken. Detta andra inlägg kommer att dyka in i de inre delarna av Googles V8 JavaScript-motor. Vi kommer också att ge några snabba tips om hur man skriver bättre JavaScript —kod-bästa praxis vårt utvecklingsteam på SessionStack följer när du bygger produkten.

översikt

en JavaScript-motor är ett program eller en tolk som utför JavaScript-kod., En JavaScript-motor kan implementeras som en standard tolk, eller just-in-time kompilator som sammanställer JavaScript till bytecode i någon form.,/li>

  • SpiderMonkey — den första JavaScript-motor, som redan på den tiden drivs Netscape Navigator, och idag befogenheter Firefox
  • JavaScriptCore — öppen källkod, som marknadsförs som Nitro och utvecklat av Apple för Safari
  • KJS — KDE: s motor som ursprungligen utvecklats av Harri Porten för KDE-projektet: s webbläsare Konqueror
  • Chakra (JScript9) — Internet Explorer
  • Chakra (JavaScript) — Microsoft Kant
  • Nashorn, öppen källkod som en del av OpenJDK, skriven av Oracle Java-Språk och Verktyg Grupp
  • JerryScript — är en kompakt motor för Internet of Things.,
  • varför skapades V8-motorn?

    V8-motorn som är byggd av Google är öppen källkod och skriven i C++. Denna motor används i Google Chrome. Till skillnad från resten av motorerna används emellertid V8 också för den populära noden.JS runtime.

    V8 utformades först för att öka prestandan för JavaScript-utförande i webbläsare. – herr talman!, För att få fart översätter V8 JavaScript-kod till effektivare maskinkod istället för att använda en tolk. Det sammanställer JavaScript-kod i maskinkod vid körning genom att implementera en JIT (Just-in-Time) kompilator som många moderna JavaScript-motorer gör som SpiderMonkey eller Rhino (Mozilla). Den största skillnaden här är att V8 inte producerar bytekod eller någon mellanliggande kod.

    V8 brukade ha två kompilatorer

    före version 5.,9 av V8 kom ut (släpptes tidigare i år), motorn använde två kompilatorer:

    • full-codegen — en enkel och mycket snabb kompilator som producerade enkel och relativt långsam maskinkod.
    • vevaxel — en mer komplex (Just-In-Time) optimera kompilator som producerade mycket optimerad kod.,l>
    • Den röda tråden gör vad du kan förvänta dig: hämta din kod, kompilerar den och sedan köra den
    • Det finns också en separat tråd för att sammanställa, så att den röda tråden kan hålla verkställande medan den förra är att optimera koden
    • Ett Profiler tråd som kommer att berätta runtime på vilka metoder vi spenderar en hel del tid, så att Vevaxeln kan optimera dem.
    • några trådar för att hantera sophämtare sveper

    När jag först köra JavaScript-kod, V8 utnyttjar full-codegen som direkt översätter de analyserade JavaScript till maskinkod utan någon förändring., Detta gör det möjligt att börja köra maskinkod mycket snabbt. Observera att V8 inte använder mellanliggande bytecode representation på detta sätt ta bort behovet av en tolk.

    När din kod har körts under en tid har profilertråden samlat tillräckligt med data för att berätta vilken metod som ska optimeras.

    därefter börjar vevaxeloptimeringar i en annan tråd. Det översätter JavaScript abstrakt syntax träd till en hög nivå statisk single-assignment (ssa) representation kallas väte och försöker optimera att väte graf. De flesta optimeringar görs på denna nivå.,

    Inlining

    den första optimeringen är inlining så mycket kod som möjligt i förväg. Inlining är processen att ersätta en samtalsplats (kodraden där funktionen kallas) med kroppen av den kallade funktionen. Detta enkla steg gör att följande optimeringar kan vara mer meningsfulla.

    Dold klass

    JavaScript är ett prototypbaserat språk: det finns inga klasser och objekt skapas med hjälp av en kloningsprocess., JavaScript är också ett dynamiskt programmeringsspråk vilket innebär att egenskaper lätt kan läggas till eller tas bort från ett objekt efter dess instantiation.

    de flesta JavaScript-tolkar använder ordboksliknande strukturer (hashfunktion baserad) för att lagra platsen för objektegenskapsvärden i minnet. Denna struktur gör att hämta värdet på en egenskap i JavaScript mer beräkningsmässigt dyrt än det skulle vara i ett icke-dynamiskt programmeringsspråk som Java eller C#., I Java bestäms alla objektegenskaper av en fast objektlayout före sammanställning och kan inte läggas till eller tas bort dynamiskt vid körning (Tja, C # har den dynamiska typen som är ett annat ämne). Som ett resultat kan värdena för Egenskaper (eller pekare till dessa egenskaper) lagras som en kontinuerlig buffert i minnet med en fast förskjutning mellan var och en. Längden på en förskjutning kan enkelt bestämmas baserat på egenskapstypen, medan detta inte är möjligt i JavaScript där en egenskapstyp kan ändras under körning.,

    eftersom det är mycket ineffektivt att använda ordböcker för att hitta platsen för objektegenskaper i minnet, använder V8 en annan metod istället: dolda klasser. Dolda klasser fungerar på samma sätt som de fasta objektlayouter (klasser) som används på språk som Java, förutom att de skapas vid körning. Låt oss nu se hur de faktiskt ser ut:

    function Point(x, y) {
    this.x = x;
    this.y = y;
    }var p1 = new Point(1, 2);

    När ”new Point(1, 2)” Anropet händer kommer V8 att skapa en dold klass som heter ”C0”.,

    inga egenskaper har definierats för punkt ännu, så ”C0” är tomt.

    När det första uttalandet ”detta.x= x ”exekveras (inuti ”Point” – funktionen), V8 kommer att skapa en andra dold klass som heter ”C1” som är baserad på ”C0”. ”C1” beskriver platsen i minnet (i förhållande till objektpekaren) där egenskapen x kan hittas., I detta fall lagras ”x” vid offset 0, vilket innebär att vid visning av ett punktobjekt i minnet som en kontinuerlig buffert motsvarar den första förskjutningen egenskapen ”x”. V8 kommer också att uppdatera ”C0” med en ”klassövergång” som säger att om en egenskap ”x” läggs till ett punktobjekt, bör den dolda klassen byta från ”C0″till ” C1″. Den dolda klassen för punktobjektet nedan är nu ”C1”.,

    varje gång en ny fastighet läggs till ett objekt uppdateras den gamla dolda klassen med en övergångsväg till den nya dolda klassen. Dolda klassövergångar är viktiga eftersom de tillåter att dolda klasser delas mellan objekt som skapas på samma sätt., Om två objekt delar en dold klass och samma egenskap läggs till dem båda, kommer övergångar att se till att båda objekten får samma nya dolda klass och all optimerad kod som följer med den.

    denna process upprepas när uttalandet ”detta.y= y ”exekveras (igen, inuti Punktfunktionen, efter” detta.x = x” uttalande).,

    en ny dold klass som heter ” C2 ”skapas, en klassövergång läggs till” C1 ”som anger att om en egenskap” y ”läggs till i ett punktobjekt (som redan innehåller egenskapen” x”) ska den dolda klassen ändras till” C2 ”och punktobjektets dolda klass uppdateras till”C2”.

    bjekt., Ta en titt på kodavsnittet nedan:

    function Point(x, y) {
    this.x = x;
    this.y = y;
    }var p1 = new Point(1, 2);
    p1.a = 5;
    p1.b = 6;var p2 = new Point(3, 4);
    p2.b = 7;
    p2.a = 8;

    nu skulle du anta att för både P1 och p2 skulle samma dolda klasser och övergångar användas. Inte direkt. För” p1″, först egenskapen” A ”kommer att läggas till och sedan egenskapen”b”. För” p2 ”tilldelas dock första” b”, följt av”a”. Således slutar” P1″ och ” p2 ” med olika dolda klasser som ett resultat av de olika övergångsvägarna. I sådana fall är det mycket bättre att initiera dynamiska egenskaper i samma ordning så att de dolda klasserna kan återanvändas.,

    inline caching

    V8 utnyttjar en annan teknik för att optimera dynamiskt skrivna språk som kallas inline caching. Inline caching bygger på observationen att upprepade samtal till samma metod tenderar att inträffa på samma typ av objekt. En djupgående förklaring av inline caching finns här.

    Vi kommer att beröra det allmänna begreppet inline caching (om du inte har tid att gå igenom den djupgående förklaringen ovan).

    Så hur fungerar det?, V8 upprätthåller en cache av typen av objekt som passerade som en parameter i de senaste metodsamtalen och använder denna information för att göra ett antagande om vilken typ av objekt som kommer att skickas som en parameter i framtiden. Om V8 kan göra ett bra antagande om vilken typ av objekt som kommer att skickas till en metod, kan det kringgå processen att räkna ut hur man kommer åt objektets egenskaper, och istället använda den lagrade informationen från tidigare sökningar till objektets dolda klass.

    Så hur är begreppen dolda klasser och inline caching relaterade?, När en metod kallas på ett specifikt objekt, måste V8-motorn utföra en sökning till den dolda klassen av det objektet för att bestämma förskjutningen för att komma åt en viss egenskap. Efter två framgångsrika samtal med samma metod till samma dolda klass utelämnar V8 den dolda klassuppslagningen och lägger helt enkelt till förskjutningen av egenskapen till objektpekaren själv. För alla framtida samtal av den metoden förutsätter V8-motorn att den dolda klassen inte har förändrats och hoppar direkt in i minnesadressen för en viss egenskap med hjälp av de förskjutningar som lagrats från tidigare sökningar., Detta ökar avsevärt exekveringshastigheten.

    inline caching är också anledningen till att det är så viktigt att objekt av samma typ delar dolda klasser. Om du skapar två objekt av samma typ och med olika dolda klasser (som vi gjorde i exemplet tidigare), V8 kommer inte att kunna använda inline caching eftersom även om de två objekten är av samma typ, deras motsvarande dolda klasser tilldela olika förskjutningar till sina egenskaper.,

    de två objekten är i princip desamma men egenskaperna ”A” och ”b” skapades i olika ordning.

    sammanställning till maskinkod

    När Vätgasgrafen är optimerad sänker vevaxeln den till en lägre nivå representation kallas litium. Det mesta av litiumplementeringen är arkitektspecifik. Registerfördelning sker på denna nivå.,

    i slutändan sammanställs litium till maskinkod. Då händer något annat som heter OSR: on-stack replacement. Innan vi började sammanställa och optimera en uppenbarligen långvarig metod, vi var sannolikt kör det. V8 kommer inte att glömma vad det bara långsamt exekveras för att börja igen med den optimerade versionen. Istället kommer det att omvandla alla sammanhang Vi har (stack, register) så att vi kan byta till den optimerade versionen i mitten av utförandet. Detta är en mycket komplex uppgift, med tanke på att bland andra optimeringar har V8 inlinjerat koden från början., V8 är inte den enda motorn som kan göra det.

    det finns skyddsåtgärder som kallas deoptimisering för att göra motsatt omvandling och återgår till den icke-optimerade koden om ett antagande som motorn gjort inte håller sant längre.

    sophämtning

    för sophämtning använder V8 en traditionell generationsstrategi för mark-and-sweep för att rengöra den gamla generationen. Markeringsfasen ska stoppa Javascript-utförandet., För att kontrollera GC-kostnader och göra utförandet stabilare använder V8 inkrementell märkning: istället för att gå hela högen, försöker markera alla möjliga objekt, går den bara en del av högen och återupptar sedan normalt utförande. Nästa GC-stopp fortsätter från där föregående heap-promenad har slutat. Detta möjliggör mycket korta pauser under det normala utförandet. Som tidigare nämnts hanteras svepfasen av separata trådar.

    tändning och TurboFan

    med lanseringen av V8 5.9 tidigare i 2017 introducerades en ny exekveringsledning., Denna nya pipeline uppnår ännu större prestandaförbättringar och betydande minnesbesparingar i verkliga JavaScript-applikationer.

    den nya exekveringsledningen är byggd ovanpå tändningen, V8: S tolk och TurboFan, V8: s nyaste optimerings kompilator.

    Du kan kolla in blogginlägget från V8-laget om ämnet här.

    Sedan version 5.,9 av V8 kom ut, full codegen och vevaxel (de tekniker som har tjänat V8 sedan 2010) har inte längre använts av V8 För JavaScript-utförande eftersom V8-laget har kämpat för att hålla jämna steg med de nya JavaScript-språken och de optimeringar som behövs för dessa funktioner.

    detta innebär att den totala V8 kommer att ha mycket enklare och mer underhållbar arkitektur framöver.

    förbättringar på webben och noden.,JS benchmarks

    dessa förbättringar är bara början. Den nya Ignition och TurboFan pipeline bana väg för ytterligare optimeringar som kommer att öka JavaScript prestanda och krympa V8 fotavtryck i både krom och Nod.js under de kommande åren.

    slutligen, här är några tips och tricks om hur man skriver väl optimerad, bättre JavaScript., Du kan enkelt härleda dessa från innehållet ovan, men här är en sammanfattning för din bekvämlighet:

    hur man skriver optimerad JavaScript

    1. ordning objektegenskaper: instansiera alltid dina objektegenskaper i samma ordning så att dolda klasser, och därefter optimerad kod, kan delas.
    2. dynamiska egenskaper: att lägga till Egenskaper till ett objekt efter instantiation kommer att tvinga en dold klassändring och sakta ner alla metoder som optimerades för den tidigare dolda klassen. Tilldela istället alla objektets egenskaper i konstruktören.,
    3. metoder: kod som utför samma metod upprepade gånger kommer att köras snabbare än kod som utför många olika metoder endast en gång (på grund av inline caching).
    4. Arrays: undvik glesa arrays där nycklar inte är inkrementella nummer. Glesa arrays som inte har alla element inuti dem är ett hashbord. Element i sådana arrayer är dyrare att komma åt. Försök också att undvika att förfördela stora arrayer. Det är bättre att växa när du går. Slutligen, ta inte bort element i matriser. Det gör nycklarna glesa.
    5. taggade värden: V8 representerar objekt och siffror med 32 bitar., Den använder lite för att veta om det är ett objekt (flagga = 1) eller ett heltal (flagga = 0) kallas SMI (litet heltal) på grund av dess 31 bitar. Sedan, om ett numeriskt värde är större än 31 bitar, kommer V8 att boxas numret, göra det till en dubbel och skapa ett nytt objekt för att sätta numret inuti. Försök att använda 31 – bitars signerade nummer när det är möjligt för att undvika den dyra boxningsoperationen i ett JS-objekt.

    Vi på SessionStack försöker följa dessa bästa metoder skriftligen mycket optimerad JavaScript-kod., Anledningen är att när du integrerar SessionStack i din produktion webbapp, det börjar spela in allt: alla DOM förändringar, användarinteraktioner, JavaScript undantag, stack spår, misslyckade nätverksförfrågningar, och felsöka meddelanden.
    med SessionStack kan du spela upp problem i dina webbappar som videor och se allt som hände med din användare. Och allt detta måste hända utan prestandaeffekter för din webbapp.
    det finns en gratis plan som låter dig komma igång gratis.

    Share

    Lämna ett svar

    Din e-postadress kommer inte publiceras. Obligatoriska fält är märkta *