Sådan JavaScript works: inside the V8-motor + 5 tips til, hvordan du skriver optimeret kode

Aug 21, 2017 · 11 min læse

Par uger siden, at vi startede en serie, der tager sigte på at grave dybere i JavaScript, og hvordan det rent faktisk virker: vi troede, at ved at kende de byggesten, af JavaScript, og hvordan de kommer til at spille sammen vil du være i stand til at skrive bedre kode og apps.,

det første indlæg i serien fokuserede på at give et overblik over motoren, runtime og opkaldsstakken. Dette andet indlæg vil dykke ned i de interne dele af Googles V8 JavaScript-motor. Vi giver også et par hurtige tip til, hvordan du skriver bedre JavaScript —kode-bedste praksis vores udviklingsteam hos SessionStack følger, Når du bygger produktet.

oversigt

en JavaScript-motor er et program eller en tolk, der udfører JavaScript-kode., En JavaScript-motor kan implementeres som en standard tolk, eller just-in-time compiler, der samler JavaScript til bytecode i en eller anden form.,/li>

  • SpiderMonkey — den første JavaScript-motor, som tilbage i de dage, drevet Netscape Navigator, og i dag beføjelser Firefox
  • Egenskaber — open source -, der markedsføres som Nitro og udviklet af Apple Safari
  • KJS — KDE ‘s motor oprindeligt udviklet af Harri Porten for KDE-projektet’ s Konqueror web-browser
  • Chakra (JScript9) — Internet Explorer
  • Chakra (JavaScript) — Microsoft Kant
  • Nashorn, open source som en del af OpenJDK, skrevet af Oracle Java Sprog og Værktøj-Gruppen
  • JerryScript — er en letvægts motor til Internet af Ting.,
  • Hvorfor blev V8-motoren oprettet?

    V8-motoren, der er bygget af Google, er open source og skrevet i C++. Denne motor bruges i Google Chrome. I modsætning til resten af motorerne bruges V8 imidlertid også til den populære knude.JS runtime.

    V8 blev først designet til at øge ydeevnen for udførelse af JavaScript inde i web-browsere., For at opnå hastighed oversætter V8 JavaScript-kode til mere effektiv maskinkode i stedet for at bruge en tolk. Den samler JavaScript-kode i maskinkode ved udførelse ved at implementere en Jit (Just-in-Time) compiler som en masse moderne JavaScript-motorer gør som SpiderMonkey eller Rhino (Mo .illa). Den største forskel her er, at V8 ikke producerer bytecode eller nogen mellemkode.

    V8 plejede at have to compilere

    før version 5.,9 af V8 kom ud (udgivet tidligere i år), motoren brugte to compilere:

    • full-codegen — en enkel og meget hurtig compiler, der producerede enkel og relativt langsom maskinkode.
    • krumtapaksel-en mere kompleks (Just-In-Time) optimering compiler, der producerede højt optimeret kode.,l>
    • Den røde tråd er, hvad du ville forvente: hent din kode, kompilere den og derefter udføre det
    • Der er også en separat tråd til at samle, så, at den røde tråd kan holde udførelse, mens førstnævnte er at optimere kode
    • Et Profiler tråd, der vil fortælle runtime på, hvilke metoder vi bruger en masse tid, således at Krumtapakslen kan optimere dem
    • Et par tråde til at håndtere Garbage Collector fejer

    Når først udførelse af JavaScript-kode, V8 benytter sig af fuld-codegen, som er direkte forbundet med de analyserede JavaScript til maskinkode uden nogen forandring., Dette gør det muligt at begynde at udføre maskinkode meget hurtigt. Bemærk, at V8 ikke bruger intermediate bytecode repræsentation på denne måde fjerner behovet for en tolk.

    Når din kode har kørt i nogen tid, har profiler-tråden samlet nok data til at fortælle, hvilken metode der skal optimeres.

    dernæst begynder Krumtapakseloptimeringer i en anden tråd. Det oversætter JavaScript abstrakt syntaks træ til et højt niveau statisk single-assignment (SSA) repræsentation kaldet Hydrogen og forsøger at optimere denne Hydrogen graf. De fleste optimeringer udføres på dette niveau.,

    Inlining

    den første optimering er inlining så meget kode som muligt på forhånd. Inlining er processen med at erstatte et opkaldssted (kodelinjen, hvor funktionen kaldes) med kroppen af den kaldte funktion. Dette enkle trin gør det muligt at følge optimeringer for at være mere meningsfuld.

    Skjult klasse

    JavaScript er et prototype-baseret sprog: der er ingen klasser og objekter, der er oprettet ved hjælp af en kloning proces., JavaScript er også et dynamisk programmeringssprog, hvilket betyder, at Egenskaber let kan tilføjes eller fjernes fra et objekt efter dets instantiering.de fleste JavaScript-tolke bruger ordboglignende strukturer (hash-funktionsbaseret) til at gemme placeringen af objektegenskabsværdier i hukommelsen. Denne struktur gør hentning af værdien af en ejendom i JavaScript mere beregningsmæssigt dyrt, end det ville være i et ikke-dynamisk programmeringssprog som Java eller C#., I Java bestemmes alle objektegenskaberne af et fast objektlayout før kompilering og kan ikke dynamisk tilføjes eller fjernes ved runtime (godt, C# har den dynamiske type, som er et andet emne). Som et resultat kan værdierne af egenskaber (eller peger på disse egenskaber) gemmes som en kontinuerlig buffer i hukommelsen med en fast forskydning mellem hver. Længden af en forskydning kan let bestemmes ud fra egenskabstypen, mens dette ikke er muligt i JavaScript, hvor en egenskabstype kan ændre sig under runtime.,

    da brug af ordbøger til at finde placeringen af objektegenskaber i hukommelsen er meget ineffektiv, bruger V8 en anden metode i stedet: skjulte klasser. Skjulte klasser fungerer på samme måde som de faste objektlayouts (klasser), der bruges på sprog som Java, medmindre de oprettes ved runtime. Lad os nu se, hvordan de faktisk ser ud:

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

    Når “nyt punkt(1, 2)” – påkaldelse sker, opretter V8 en skjult klasse kaldet “C0”.,

    Ingen ejendomme er blevet defineret til Punkt endnu, så “C0” er tom.

    når den første sætning “Dette.= = = “udføres (inde i” Point “- funktionen), V8 opretter en anden skjult klasse kaldet” C1″, der er baseret på”C0″. “C1” beskriver placeringen i hukommelsen (i forhold til objektpekeren), hvor ejendommen can kan findes., I dette tilfælde gemmes “0”ved forskydning 0, hvilket betyder, at når man ser et punktobjekt i hukommelsen som en kontinuerlig buffer, svarer den første forskydning til ejendommen”.”. V8 vil også opdatere ” C0 “med en” klasseovergang”, der siger, at hvis en ejendom”. “føjes til et punktobjekt, skal den skjulte klasse skifte fra” C0 “til”C1”. Den skjulte klasse for punktobjektet nedenfor er nu “C1”.,

    Hver gang en ny ejendom, der er føjet til et objekt, den gamle skjulte klasse er opdateret med en overgang vej til den nye skjulte klasse. Skjulte klasseovergange er vigtige, fordi de tillader, at Skjulte klasser deles mellem objekter, der oprettes på samme måde., Hvis to objekter deler en skjult klasse, og den samme egenskab føjes til dem begge, vil overgange sikre, at begge objekter får den samme nye skjulte klasse og al den optimerede kode, der følger med den.

    denne proces gentages, når sætningen “Dette.y = y “udføres (igen, inde i Punktfunktionen, efter” dette.statement = statement” erklæring).,

    en ny skjult klasse kaldet “C2” oprettes, en klasseovergang tilføjes til “C1” om, at hvis en ejendom “y” tilføjes til et Punktobjekt (der allerede indeholder ejendom”.”), skal den skjulte klasse ændres til “C2”, og punktobjektets skjulte klasse opdateres til “C2”.

    Skjulte klasse overgange er afhængig af den rækkefølge, i hvilke egenskaber der er føjet til et objekt., Se på kodestykket nedenfor:

    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 antager du, at for både p1 og p2 ville de samme skjulte klasser og overgange blive brugt. Ikke rigtig. For “p1 “tilføjes først ejendommen” a “og derefter ejendommen”B”. For “p2 “tildeles imidlertid først” b “efterfulgt af”a”. Således ender “P1″ og ” P2 ” med forskellige skjulte klasser som et resultat af de forskellige overgangsstier. I sådanne tilfælde er det meget bedre at initialisere dynamiske egenskaber i samme rækkefølge, så de skjulte klasser kan genbruges.,

    Inline caching

    V8 udnytter en anden teknik til at optimere dynamisk indtastede sprog kaldet inline caching. Inline caching er afhængig af observationen, at gentagne opkald til den samme metode har tendens til at forekomme på den samme type objekt. En dybdegående forklaring af inline caching kan findes her.

    vi kommer til at berøre det generelle koncept med inline caching (hvis du ikke har tid til at gennemgå den dybdegående forklaring ovenfor).

    så hvordan virker det?, V8 opretholder en cache af den type objekter, der blev sendt som en parameter i nylige metodekald, og bruger disse oplysninger til at antage, hvilken type objekt der vil blive sendt som en parameter i fremtiden. Hvis V8 er i stand til at gøre en god antagelse om den type objekt, der vil blive videregivet til en metode, kan det omgå processen med at finde ud af, hvordan man får adgang til objektets egenskaber, og i stedet bruge de lagrede oplysninger fra tidligere opslag til objektets skjulte klasse.

    så hvordan er begreberne skjulte klasser og inline caching relateret?, Hver gang en metode kaldes på et specifikt objekt, skal V8-motoren udføre et opslag til den skjulte klasse af det objekt for at bestemme forskydningen for adgang til en bestemt egenskab. Efter to vellykkede opkald af samme metode til den samme skjulte klasse udelader V8 den skjulte klasseopslag og tilføjer blot forskydningen af ejendommen til objektpekeren selv. For alle fremtidige opkald af denne metode antager V8-motoren, at den skjulte klasse ikke er ændret, og hopper direkte ind i hukommelsesadressen for en bestemt egenskab ved hjælp af forskydningerne, der er gemt fra tidligere opslag., Dette øger udførelsen hastighed.

    Inline caching er også grunden til, at det er så vigtigt, at objekter af samme type deler skjulte klasser. Hvis du opretter to objekter af samme type og med forskellige skjulte klasser (som vi gjorde i eksempel tidligere), V8 vil ikke være i stand til at bruge inline caching, fordi selvom de to objekter af samme type, deres tilsvarende skjulte klasser tildele forskellige forskydninger til deres ejendomme.,

    De to objekter er stort set den samme, men “a” og “b” egenskaber blev skabt i en anden rækkefølge.

    kompilering til maskinkode

    Når Brintgrafen er optimeret, sænker krumtapakslen den til en repræsentation på lavere niveau kaldet Lithium. Det meste af Litiumimplementeringen er arkitekturspecifik. Registerallokering sker på dette niveau.,

    i sidste ende er Lithium kompileret til maskinkode. Så sker der noget andet kaldet OSR: on-stack udskiftning. Før vi begyndte at kompilere og optimere en åbenlyst langvarig metode, kørte vi sandsynligvis den. V8 vil ikke glemme, hvad det bare langsomt udføres for at starte igen med den optimerede version. I stedet vil det omdanne al den kontekst, vi har (stak, registre), så vi kan skifte til den optimerede version midt i udførelsen. Dette er en meget kompleks opgave, idet man husker, at V8 blandt andre optimeringer oprindeligt har indlejret koden., V8 er ikke den eneste motor, der er i stand til at gøre det.

    Der er sikkerhedsforanstaltninger kaldet deoptimi .ation for at gøre den modsatte transformation og vender tilbage til den ikke-optimerede kode, hvis en antagelse, som motoren lavede, ikke holder sandt mere.

    Garbage collection

    til garbage collection bruger V8 en traditionel generationsmetode af mark-and-S .eep for at rense den gamle generation. Markeringsfasen skal stoppe JavaScript-udførelsen., For at kontrollere GC-omkostninger og gøre udførelsen mere stabil bruger V8 trinvis markering: i stedet for at gå hele bunken, forsøge at markere ethvert muligt objekt, går det kun en del af bunken og genoptager derefter normal udførelse. Det næste GC-stop fortsætter, hvorfra den forrige heap walkalk er stoppet. Dette giver mulighed for meget korte pauser under den normale udførelse. Som tidligere nævnt håndteres fejefasen af separate tråde.

    tænding og TurboFan

    Med udgivelsen af V8 5.9 tidligere i 2017 blev der introduceret en ny udførelsesrørledning., Denne nye pipeline opnår endnu større ydelsesforbedringer og betydelige hukommelsesbesparelser i JavaScript-applikationer i den virkelige verden.

    den nye e .ecution pipeline er bygget oven på Ignition, V8S tolk og TurboFan, V8s nyeste optimeringskompiler.

    Du kan tjekke blogindlægget fra V8-teamet om emnet her.

    siden version 5.,9 V8 kom ud, fuld-codegen og Krumtap (de teknologier, der har tjent V8 siden 2010) har ikke længere været brugt af V8 for udførelse af JavaScript som V8 hold har kæmpet for at holde trit med de nye JavaScript-sproget funktioner og optimeringer behov for disse funktioner.

    dette betyder, at den samlede V8 vil have meget enklere og mere vedligeholdelig arkitektur fremadrettet.

    Forbedringer på Web og Node.,js benchmarks

    disse forbedringer er kun starten. Den nye tændings-og TurboFan-rørledning baner vejen for yderligere optimeringer, der vil øge JavaScript-ydelsen og krympe V8 ‘ s fodaftryk i både Chrome og Node.js i de kommende år.

    endelig er her nogle tip og tricks til, hvordan man skriver godt optimeret, bedre JavaScript., Du kan nemt udlede disse fra indholdet ovenfor, men her er et resume for din bekvemmelighed:

    Sådan skriver du Optimeret JavaScript

    1. rækkefølge af objektegenskaber: instantiser altid dine objektegenskaber i samme rækkefølge, så skjulte klasser og efterfølgende optimeret kode kan deles.
    2. dynamiske egenskaber: tilføjelse af egenskaber til et objekt efter instantiering vil tvinge en skjult klasseændring og bremse eventuelle metoder, der blev optimeret til den tidligere skjulte klasse. I stedet tildele alle et objekts egenskaber i sin konstruktør.,
    3. metoder: kode, der udfører den samme metode gentagne gange, kører hurtigere end kode, der kun udfører mange forskellige metoder oncen gang (på grund af inline caching).
    4. Arrays: undgå sparsomme arrays, hvor tasterne ikke er trinvise tal. Sparse arrays, som ikke har alle elementer inde i dem er en hash tabel. Elementer i sådanne arrays er dyrere at få adgang til. Prøv også at undgå forudfordeling af store arrays. Det er bedre at vokse som du går. Endelig skal du ikke slette elementer i arrays. Det gør nøglerne sparsomme.
    5. mærkede værdier: V8 repræsenterer objekter og tal med 32 bit., Det bruger lidt at vide, om det er et objekt (flag = 1) eller et heltal (flag = 0) kaldet SMI (lille heltal) på grund af dets 31 bit. Så hvis en numerisk værdi er større end 31 bit, vil V8 boks nummeret, gøre det til en dobbelt og oprette et nyt objekt for at sætte nummeret inde. Prøv at bruge 31 bit signerede numre når det er muligt for at undgå den dyre boksning operation i en JS objekt.

    Vi ved SessionStack forsøger at følge disse bedste fremgangsmåder ved at skrive stærkt optimeret JavaScript-kode., Årsagen er, at når du først integrerer SessionStack i din Produktions webebapp, begynder den at optage alt: alle DOM-ændringer, brugerinteraktioner, JavaScript-undtagelser, stak spor, mislykkede netværksanmodninger og fejlfindingsmeddelelser.
    med SessionStack kan du afspille problemer i dine appebapps som videoer og se alt, hvad der skete med din bruger. Og alt dette skal ske uden præstationspåvirkning for din .ebapp.
    der er en gratis plan, der giver dig mulighed for at komme i gang gratis.

    Share

    Skriv et svar

    Din e-mailadresse vil ikke blive publiceret. Krævede felter er markeret med *