Hoe JavaScript werkt: binnen de V8-motor + 5 tips over het schrijven van geoptimaliseerde code

Aug 21, 2017 · 11 min lezen

Paar weken geleden zijn we gestart met een serie gericht op het dieper graven in JavaScript en hoe het eigenlijk werkt: we dachten dat door het kennen van de bouwstenen van JavaScript en hoe komen ze om samen te spelen kun je beter schrijven code en apps.,

de eerste post van de serie was gericht op het geven van een overzicht van de motor, de runtime en de call stack. Deze tweede post zal duiken in de interne delen van Google ‘ s V8 JavaScript engine. We geven ook een paar snelle tips over het schrijven van betere JavaScript —code-best practices ons ontwikkelingsteam bij SessionStack volgt bij het bouwen van het product.

overzicht

een JavaScript-engine is een programma of een interpreter die JavaScript-code uitvoert., Een JavaScript engine kan worden geà mplementeerd als een standaard interpreter, of just-in-time compiler die JavaScript compileert om bytecode in een of andere vorm.,/li>

  • SpiderMonkey — de eerste JavaScript-engine, die in de dagen aangedreven Netscape Navigator, en vandaag bevoegdheden Firefox
  • Planningsprogramma — open source, op de markt gebracht als Nitro en is ontwikkeld door Apple voor Safari
  • KJS — KDE ‘ s motor is oorspronkelijk ontwikkeld door Harri Porten voor het KDE-project de Konqueror browser
  • Chakra (JScript9) — Internet Explorer
  • Chakra (JavaScript) — Microsoft Rand
  • Nashorn, open source als onderdeel van OpenJDK, geschreven door Oracle Java Talen en Gereedschap Groep
  • JerryScript — is een lichtgewicht motor voor het Internet van de Dingen.,
  • Waarom is de V8-Engine gemaakt?

    De V8 Engine die is gebouwd door Google is open source en geschreven in C++. Deze motor wordt gebruikt in Google Chrome. In tegenstelling tot de rest van de motoren, echter, V8 wordt ook gebruikt voor de populaire Node.js runtime.

    V8 werd voor het eerst ontworpen om de prestaties van JavaScript-uitvoering binnen webbrowsers te verhogen., Om snelheid te verkrijgen, vertaalt V8 JavaScript-code in efficiëntere machine code in plaats van het gebruik van een interpreter. Het compileert JavaScript-code in machine code bij uitvoering door het implementeren van een JIT (Just-In-Time) compiler zoals veel moderne JavaScript-engines doen zoals SpiderMonkey of Rhino (Mozilla). Het belangrijkste verschil hier is dat V8 geen bytecode of een tussenliggende code produceert.

    V8 had voor versie 5 twee compilers

    .,9 van V8 kwam uit (eerder dit jaar uitgebracht), de motor gebruikt twee compilers:

    • full-codegen — een eenvoudige en zeer snelle compiler die eenvoudige en relatief langzame machine code geproduceerd.
    • krukas-een complexere (Just-In-Time) optimalisatie compiler die zeer geoptimaliseerde code produceerde.,l>
    • De belangrijkste draad doet wat je zou verwachten: haal uw code compileren en uitvoeren
    • Er is ook een aparte thread voor het samenstellen, zodat de main thread kan blijven uitvoeren terwijl de eerste het optimaliseren van de code
    • Een Profiler draad dat vertelt de runtime op die methoden die we besteden veel tijd, zodat Krukas kan optimaliseren
    • Een paar draden te hanteren Garbage Collector veegt

    Bij de eerste uitvoering van de JavaScript-code, V8 maakt gebruik van full-codegen, die zich direct vertaalt de geparseerd JavaScript in machine code zonder enige verandering., Dit maakt het mogelijk om te beginnen met het uitvoeren van machine code zeer snel. Merk op dat V8 geen tussenliggende bytecode representatie gebruikt op deze manier waardoor de noodzaak voor een interpreter wordt weggenomen.

    wanneer uw code een tijdje draait, heeft de profiler thread genoeg gegevens verzameld om te vertellen welke methode geoptimaliseerd moet worden.

    volgende, krukas optimalisaties beginnen in een andere thread. Het vertaalt de JavaScript abstracte syntaxis boom naar een high-level statische single-assignment (SSA) representatie genaamd waterstof en probeert die waterstof grafiek te optimaliseren. De meeste optimalisaties worden gedaan op dit niveau.,

    Inlining

    de eerste optimalisatie is zoveel mogelijk code van tevoren inlinen. Inlining is het proces van het vervangen van een aanroep site (de regel van de code waar de functie wordt aangeroepen) met de body van de aangeroepen functie. Deze eenvoudige stap maakt het mogelijk na optimalisaties om meer betekenis te zijn.

    Verborgen klasse

    JavaScript is een prototype-gebaseerde taal: er zijn geen klassen en objecten worden gemaakt met behulp van een proces., JavaScript is ook een dynamische programmeertaal, wat betekent dat eigenschappen gemakkelijk kunnen worden toegevoegd of verwijderd uit een object na de instantiatie.

    De meeste JavaScript-tolken gebruiken woordenboekachtige structuren (gebaseerd op hash-functie) om de locatie van objecteigenschappen in het geheugen op te slaan. Deze structuur maakt het ophalen van de waarde van een eigenschap in JavaScript rekenkundig duurder dan het zou zijn in een niet-dynamische programmeertaal zoals Java of C#., In Java worden alle objecteigenschappen bepaald door een vaste objectlay-out voor compilatie en kunnen ze niet dynamisch worden toegevoegd of verwijderd tijdens runtime (nou ja, C# heeft het dynamische type dat een ander onderwerp is). Als gevolg hiervan kunnen de waarden van Eigenschappen (of pointers naar die eigenschappen) worden opgeslagen als een continue buffer in het geheugen met een vaste-offset tussen elk. De lengte van een offset kan eenvoudig worden bepaald op basis van het eigenschapstype, terwijl dit niet mogelijk is in JavaScript waar een eigenschapstype kan veranderen tijdens runtime.,

    omdat het gebruik van woordenboeken om de locatie van objecteigenschappen in het geheugen te vinden erg inefficiënt is, gebruikt V8 een andere methode: verborgen klassen. Verborgen klassen werken op dezelfde manier als de vaste object lay-outs (klassen) gebruikt in talen zoals Java, behalve dat ze worden gemaakt tijdens runtime. Laten we nu eens kijken hoe ze er eigenlijk uitzien:

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

    zodra de” new Point(1, 2) “aanroep plaatsvindt, zal V8 een verborgen klasse aanmaken met de naam”C0”.,

    Er zijn nog geen eigenschappen gedefinieerd voor punt, dus “C0” is leeg.

    zodra het eerste statement ” this.x = x “wordt uitgevoerd (binnen de “Point” functie), V8 zal een tweede verborgen klasse genaamd “C1” die is gebaseerd op “C0”. “C1” beschrijft de locatie in het geheugen (ten opzichte van de object pointer) waar de eigenschap x kan worden gevonden., In dit geval wordt “x” opgeslagen op offset 0, wat betekent dat wanneer een puntobject in het geheugen wordt bekeken als een continue buffer, de eerste offset overeenkomt met eigenschap “x”. V8 zal ook “C0” updaten met een “class transition” die stelt dat als een eigenschap “x” wordt toegevoegd aan een puntobject, de verborgen klasse moet overschakelen van “C0” naar “C1”. De verborgen klasse voor het puntobject hieronder is nu “C1”.,

    elke keer dat een nieuwe eigenschap aan een object wordt toegevoegd, wordt de oude verborgen klasse wordt bijgewerkt met een overgangspad naar de nieuwe verborgen klasse. Verborgen klassenovergangen zijn belangrijk omdat ze toestaan dat verborgen klassen worden gedeeld tussen objecten die op dezelfde manier zijn gemaakt., Als twee objecten een verborgen klasse delen en dezelfde eigenschap aan beide objecten wordt toegevoegd, zorgt overgangen ervoor dat beide objecten dezelfde nieuwe verborgen klasse en alle bijbehorende geoptimaliseerde code ontvangen.

    dit proces wordt herhaald wanneer het statement ” this.y = y “wordt uitgevoerd (opnieuw, binnen de Puntfunctie, na de” dit.x = x” statement).,

    een nieuwe verborgen klasse genaamd ” C2 “wordt gemaakt, een klasse overgang wordt toegevoegd aan” C1 “waarin staat dat als een eigenschap” y “wordt toegevoegd aan een punt object (dat al eigenschap” x “bevat) dan de verborgen klasse moet veranderen in” C2″, en de verborgen klasse van het punt object wordt bijgewerkt naar”C2″.

    verborgen klasseovergangen zijn afhankelijk van de volgorde waarin eigenschappen aan een object worden toegevoegd., Neem een kijkje op het onderstaande codefragment:

    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 zou je aannemen dat Voor zowel p1 als p2 dezelfde verborgen klassen en overgangen gebruikt zouden worden. Nou, niet echt. Voor ” p1 “wordt eerst de eigenschap” a “toegevoegd en vervolgens de eigenschap”b”. Voor ” p2 “wordt echter eerst” b “toegewezen, gevolgd door”a”. Dus,” p1 “en” p2 ” eindigen met verschillende verborgen klassen als gevolg van de verschillende overgangspaden. In dergelijke gevallen is het veel beter om dynamische eigenschappen in dezelfde volgorde te initialiseren, zodat de verborgen klassen kunnen worden hergebruikt.,

    Inline caching

    V8 maakt gebruik van een andere techniek voor het optimaliseren van dynamisch getypte talen, genaamd inline caching. Inline caching is gebaseerd op de observatie dat herhaalde oproepen naar dezelfde methode meestal plaatsvinden op hetzelfde type object. Een uitgebreide uitleg van inline caching vindt u hier.

    We gaan ingaan op het algemene concept van inline caching (voor het geval je niet de tijd hebt om door de uitgebreide uitleg hierboven te gaan).

    dus hoe werkt het?, V8 onderhoudt een cache van het type objecten die zijn doorgegeven als een parameter in recente methode oproepen en gebruikt deze informatie om een aanname over het type object dat zal worden doorgegeven als een parameter in de toekomst te maken. Als V8 in staat is om een goede veronderstelling te maken over het type object dat zal worden doorgegeven aan een methode, kan het omzeilen van het proces van het uitzoeken hoe toegang te krijgen tot de eigenschappen van het object, en in plaats daarvan, gebruik maken van de opgeslagen informatie van eerdere lookups om het object verborgen klasse.

    dus hoe zijn de concepten van verborgen klassen en inline caching gerelateerd?, Wanneer een methode wordt aangeroepen op een specifiek object, moet de V8-engine een lookup uitvoeren naar de verborgen klasse van dat object om de offset te bepalen voor de toegang tot een specifieke eigenschap. Na twee succesvolle aanroepen van dezelfde methode naar dezelfde verborgen klasse, V8 weglaat de verborgen klasse lookup en voegt gewoon de offset van de eigenschap aan de object pointer zelf. Voor alle toekomstige oproepen van die methode, de V8-engine gaat ervan uit dat de verborgen klasse is niet veranderd, en springt direct in het geheugen adres voor een specifieke eigenschap met behulp van de offsets opgeslagen van eerdere lookups., Dit verhoogt de uitvoeringssnelheid aanzienlijk.

    Inline caching is ook de reden waarom het zo belangrijk is dat objecten van hetzelfde type verborgen klassen delen. Als u twee objecten van hetzelfde type en met verschillende verborgen klassen maakt (zoals we in het voorbeeld eerder deden), zal V8 niet in staat zijn om inline caching te gebruiken omdat, hoewel de twee objecten van hetzelfde type zijn, hun overeenkomstige verborgen klassen verschillende offsets toewijzen aan hun eigenschappen.,

    de twee objecten zijn in principe hetzelfde, maar de “a” en “B” eigenschappen werden gemaakt in verschillende volgorde.

    compilatie naar machinecode

    zodra de Waterstofgrafiek is geoptimaliseerd, verlaagt de krukas deze tot een lagere weergave, Lithium genaamd. Het grootste deel van de Lithiumimplementatie is architectuurspecifiek. Register allocatie gebeurt op dit niveau.,

    uiteindelijk wordt Lithium gecompileerd in machinecode. Dan gebeurt er iets anders genaamd OSR: on-stack replacement. Voordat we begonnen met het compileren en optimaliseren van een duidelijk langlopende methode, waren we waarschijnlijk het uitvoeren van het. V8 is niet van plan om te vergeten wat het gewoon langzaam uitgevoerd om opnieuw te beginnen met de geoptimaliseerde versie. In plaats daarvan transformeert het alle context die we hebben (stack, registers), zodat we kunnen overschakelen naar de geoptimaliseerde versie in het midden van de uitvoering. Dit is een zeer complexe taak, met in gedachten dat onder andere optimalisaties, V8 heeft inline de code in eerste instantie., V8 is niet de enige motor die dat kan.

    Er zijn voorzorgsmaatregelen die deoptimalisatie worden genoemd om de omgekeerde transformatie te maken en terug te keren naar de niet-geoptimaliseerde code in het geval een aanname van de motor niet meer waar is.

    Garbage collection

    voor garbage collection gebruikt V8 een traditionele generatiebenadering van mark-and-sweep om de oude generatie schoon te maken. De markering fase wordt verondersteld om de uitvoering van JavaScript te stoppen., Om de kosten van GC te beheersen en de uitvoering stabieler te maken, gebruikt V8 incrementele markering: in plaats van de hele hoop te lopen, proberen om elk mogelijk object te markeren, loopt het slechts een deel van de hoop, dan hervat de normale uitvoering. De volgende GC stop zal doorgaan vanaf waar de vorige heap walk is gestopt. Dit zorgt voor zeer korte pauzes tijdens de normale uitvoering. Zoals eerder vermeld, wordt de sweep-fase behandeld door afzonderlijke schroefdraden.

    ontsteking en TurboFan

    met de release van V8 5.9 eerder in 2017 werd een nieuwe uitvoeringspijplijn geïntroduceerd., Deze nieuwe pijplijn bereikt nog grotere prestatieverbeteringen en aanzienlijke geheugenbesparingen in echte JavaScript-toepassingen.

    de nieuwe uitvoeringspijplijn is gebouwd bovenop Ignition, V8 ’s interpreter en TurboFan, V8′ s nieuwste optimalisatie compiler.

    u kunt de blogpost van het V8-team over het onderwerp hier bekijken.

    sinds versie 5.,9 van V8 kwam uit, full-codegen en krukas (de technologieën die V8 hebben gediend sinds 2010) zijn niet langer gebruikt door V8 voor JavaScript uitvoering als de V8 team heeft moeite om gelijke tred te houden met de nieuwe JavaScript-taal functies en de optimalisaties die nodig zijn voor deze functies.

    Dit betekent dat over het algemeen V8 veel eenvoudiger en beter te onderhouden architectuur zal hebben in de toekomst.

    verbeteringen op Web en Node.,js benchmarks

    deze verbeteringen zijn slechts het begin. De nieuwe ontsteking en TurboFan pipeline effenen de weg voor verdere optimalisaties die JavaScript-prestaties zullen stimuleren en de voetafdruk van V8 in zowel Chrome als Node zal verkleinen.js in de komende jaren.

    tot slot, hier zijn enkele tips en trucs over het schrijven van goed geoptimaliseerde, betere JavaScript., U kunt deze gemakkelijk afleiden uit de bovenstaande inhoud, maar hier is een samenvatting voor uw gemak:

    hoe kunt u geoptimaliseerd JavaScript schrijven

    1. volgorde van objecteigenschappen: instantiate uw objecteigenschappen altijd in dezelfde volgorde, zodat verborgen klassen, en vervolgens geoptimaliseerde code, kunnen worden gedeeld.
    2. dynamische eigenschappen: het toevoegen van eigenschappen aan een object na instantiatie zal een verandering van verborgen klasse forceren en alle methoden vertragen die geoptimaliseerd zijn voor de vorige verborgen klasse. Wijs in plaats daarvan alle eigenschappen van een object toe aan de constructor.,
    3. methoden: code die dezelfde methode herhaaldelijk uitvoert zal sneller draaien dan code die veel verschillende methoden slechts één keer uitvoert (vanwege inline caching).
    4. Arrays: vermijd schaarse arrays waar sleutels geen incrementele getallen zijn. Sparse arrays die niet elk element in hen hebben zijn een hash tabel. Elementen in dergelijke arrays zijn duurder om toegang te krijgen. Probeer ook te voorkomen dat grote arrays vooraf worden toegewezen. Het is beter om te groeien als je gaat. Tot slot, verwijder geen elementen in arrays. Het maakt de sleutels schaars.
    5. Tagged values: V8 representeert objecten en getallen met 32 bits., Het gebruikt een bit om te weten of het een object is (flag = 1) of een geheel getal (flag = 0) genaamd SMI (SMall Integer) vanwege zijn 31 bits. Dan, als een numerieke waarde groter is dan 31 bits, V8 zal vakje het nummer, draaien in een dubbele en het creëren van een nieuw object om het nummer in te zetten. Probeer te gebruiken 31 bit gesigneerde nummers waar mogelijk om de dure boksen operatie te voorkomen in een js object.

    bij SessionStack proberen we deze best practices te volgen door het schrijven van sterk geoptimaliseerde JavaScript-code., De reden is dat zodra u SessionStack integreert in uw productie web app, het begint met het opnemen van alles: Alle DOM veranderingen, gebruikersinteracties, JavaScript uitzonderingen, stack traces, mislukte netwerk verzoeken, en debug berichten.
    Met SessionStack kunt u problemen in uw webapps opnieuw afspelen als video ‘ s en alles zien wat er met uw gebruiker is gebeurd. En dit alles moet gebeuren zonder prestatie-impact voor uw web-app.
    er is een gratis plan waarmee u gratis kunt beginnen.

    Share

    Geef een reactie

    Het e-mailadres wordt niet gepubliceerd. Vereiste velden zijn gemarkeerd met *