Come funziona JavaScript: all’interno del motore V8 + 5 consigli su come scrivere codice ottimizzato

Ago 21, 2017 · 11 min leggere

Un paio di settimane fa abbiamo iniziato una serie volta a scavare più in profondità JavaScript e come funziona: abbiamo pensato che conoscendo la creazione di blocchi di codice JavaScript e come essi vengono a giocare insieme, sarete in grado di scrivere codice migliore e applicazioni.,

Il primo post della serie si è concentrato sulla fornitura di una panoramica del motore, del runtime e dello stack di chiamate. Questo secondo post si immergerà nelle parti interne del motore JavaScript V8 di Google. Forniremo anche alcuni suggerimenti rapidi su come scrivere codice JavaScript migliore —best practice il nostro team di sviluppo di SessionStack segue quando si costruisce il prodotto.

Panoramica

Un motore JavaScript è un programma o un interprete che esegue il codice JavaScript., Un motore JavaScript può essere implementato come interprete standard o compilatore just-in-time che compila JavaScript in bytecode in qualche forma.,/li>

  • SpiderMonkey — il primo motore JavaScript, che nei giorni powered Netscape Navigator, e oggi i poteri di Firefox
  • JavaScriptCore — open source, commercializzato come Nitro e sviluppato da Apple per Safari
  • KJS — KDE motore sviluppato originariamente da Harri Porten per il progetto KDE del browser web Konqueror
  • Chakra (JScript9) — Internet Explorer
  • Chakra (JavaScript) — Microsoft Bordo
  • Nashorn, open source come parte di OpenJDK, scritto da Oracle Java Linguaggi e Tool Group
  • JerryScript — è un motore leggero per l’Internet delle Cose.,
  • Perché è stato creato il motore V8?

    Il motore V8 che è costruito da Google è open source e scritto in C++. Questo motore è utilizzato all’interno di Google Chrome. A differenza del resto dei motori, tuttavia, V8 viene utilizzato anche per il nodo popolare.runtime js.

    V8 è stato progettato in primo luogo per aumentare le prestazioni dell’esecuzione di JavaScript all’interno del browser web., Per ottenere velocità, V8 traduce il codice JavaScript in un codice macchina più efficiente invece di utilizzare un interprete. Compila il codice JavaScript in codice macchina all’esecuzione implementando un compilatore JIT (Just-In-Time) come fanno molti motori JavaScript moderni come SpiderMonkey o Rhino (Mozilla). La differenza principale qui è che V8 non produce bytecode o alcun codice intermedio.

    V8 aveva due compilatori

    Prima della versione 5.,9 di V8 è uscito (rilasciato all’inizio di quest’anno), il motore ha utilizzato due compilatori:

    • full-codegen — un compilatore semplice e molto veloce che ha prodotto codice macchina semplice e relativamente lento.
    • Crankshaft — un compilatore di ottimizzazione più complesso (Just-In-Time) che ha prodotto codice altamente ottimizzato.,l>
    • Il thread principale non è quello che vi aspettereste: recuperare il tuo codice, compilarlo ed eseguirlo
    • C’è anche un thread separato per la compilazione, in modo che il thread principale si può continuare ad eseguire, mentre il primo è l’ottimizzazione del codice
    • Un Profiler thread che racconterà il runtime su che metodi abbiamo spendere un sacco di tempo in modo che l’Albero motore può ottimizzare
    • Un paio di thread per gestire il Garbage Collector spazza

    Quando prima di eseguire il codice JavaScript V8 sfrutta a pieno codegen che si traduce direttamente analizzata JavaScript in codice macchina senza alcuna trasformazione., Ciò consente di iniziare a eseguire codice macchina molto velocemente. Si noti che V8 non utilizza la rappresentazione bytecode intermedia in questo modo eliminando la necessità di un interprete.

    Quando il codice è stato eseguito per un po ‘ di tempo, il thread del profiler ha raccolto dati sufficienti per indicare quale metodo dovrebbe essere ottimizzato.

    Successivamente, le ottimizzazioni dell’albero motore iniziano in un altro thread. Traduce l’albero della sintassi astratta JavaScript in una rappresentazione SSA (Static Single-Assignment) di alto livello chiamata Hydrogen e cerca di ottimizzare quel grafico a idrogeno. La maggior parte delle ottimizzazioni sono fatte a questo livello.,

    Inlining

    La prima ottimizzazione è inlining quanto più codice possibile in anticipo. Inlining è il processo di sostituzione di un sito di chiamata (la riga di codice in cui viene chiamata la funzione) con il corpo della funzione chiamata. Questo semplice passaggio consente di seguire le ottimizzazioni per essere più significativi.

    Nascosto classe

    JavaScript è un linguaggio basato sui prototipi: non ci sono classi e gli oggetti vengono creati utilizzando un processo di clonazione., JavaScript è anche un linguaggio di programmazione dinamico che significa che le proprietà possono essere facilmente aggiunte o rimosse da un oggetto dopo la sua istanziazione.

    La maggior parte degli interpreti JavaScript utilizza strutture simili a dizionari (basate su funzioni hash) per memorizzare la posizione dei valori delle proprietà dell’oggetto nella memoria. Questa struttura rende il recupero del valore di una proprietà in JavaScript più computazionalmente costoso di quanto sarebbe in un linguaggio di programmazione non dinamico come Java o C#., In Java, tutte le proprietà dell’oggetto sono determinate da un layout oggetto fisso prima della compilazione e non possono essere aggiunte o rimosse dinamicamente in fase di runtime (beh, c# ha il tipo dinamico che è un altro argomento). Di conseguenza, i valori delle proprietà (o puntatori a tali proprietà) possono essere memorizzati come un buffer continuo nella memoria con un offset fisso tra ciascuno. La lunghezza di un offset può essere facilmente determinata in base al tipo di proprietà, mentre questo non è possibile in JavaScript dove un tipo di proprietà può cambiare durante il runtime.,

    Poiché l’utilizzo dei dizionari per trovare la posizione delle proprietà dell’oggetto nella memoria è molto inefficiente, V8 utilizza invece un metodo diverso: classi nascoste. Le classi nascoste funzionano in modo simile ai layout di oggetti fissi (classi) utilizzati in linguaggi come Java, tranne che vengono creati in fase di runtime. Ora, vediamo come sono effettivamente:

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

    Una volta che si verifica l’invocazione “new Point(1, 2)”, V8 creerà una classe nascosta chiamata “C0”.,

    Nessuna proprietà sono state definite per il Punto di sicurezza, così “C0” è vuota.

    Una volta che la prima istruzione ” questo.x = x “viene eseguito (all’interno della funzione” Point”), V8 creerà una seconda classe nascosta chiamata” C1 “basata su”C0”. “C1” descrive la posizione nella memoria (relativa al puntatore dell’oggetto) in cui è possibile trovare la proprietà x., In questo caso, “x” viene memorizzato nell’offset 0, il che significa che quando si visualizza un oggetto punto nella memoria come buffer continuo, il primo offset corrisponderà alla proprietà “x”. V8 aggiornerà anche ” C0 “con una” transizione di classe “che afferma che se una proprietà” x “viene aggiunta a un oggetto point, la classe nascosta dovrebbe passare da” C0 “a”C1”. La classe nascosta per l’oggetto punto qui sotto è ora “C1”.,

    Ogni volta che una nuova proprietà viene aggiunto a un oggetto, il vecchio nascosti classe è aggiornata con un percorso di transizione verso il nuovo nascosto classe. Le transizioni di classe nascoste sono importanti perché consentono di condividere le classi nascoste tra gli oggetti creati allo stesso modo., Se due oggetti condividono una classe nascosta e la stessa proprietà viene aggiunta a entrambi, transitions garantirà che entrambi gli oggetti ricevano la stessa nuova classe nascosta e tutto il codice ottimizzato fornito con esso.

    Questo processo viene ripetuto quando l’istruzione ” this.y = y “viene eseguito (di nuovo, all’interno della funzione Point, dopo” this.x = x ” istruzione).,

    Viene creata una nuova classe nascosta chiamata “C2”, una transizione di classe viene aggiunta a “C1” affermando che se una proprietà “y” viene aggiunta a un oggetto Point (che contiene già la proprietà “x”), la classe hidden dovrebbe cambiare in “C2” e la classe nascosta dell’oggetto point viene aggiornata a “C2”.

    Nascosto classe transizioni dipendono dall’ordine in cui le proprietà sono aggiunti a un oggetto., Dai un’occhiata al frammento di codice qui sotto:

    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;

    Ora, si presume che per p1 e p2 vengano utilizzate le stesse classi e transizioni nascoste. Beh, non proprio. Per “p1”, prima verrà aggiunta la proprietà” a “e quindi la proprietà”b”. Per “p2″, tuttavia, viene assegnato il primo” b”, seguito da”a”. Pertanto, “p1” e” p2 ” finiscono con diverse classi nascoste come risultato dei diversi percorsi di transizione. In questi casi, è molto meglio inizializzare le proprietà dinamiche nello stesso ordine in modo che le classi nascoste possano essere riutilizzate.,

    Inline caching

    V8 sfrutta un’altra tecnica per ottimizzare i linguaggi tipizzati dinamicamente chiamata inline caching. Il caching inline si basa sull’osservazione che le chiamate ripetute allo stesso metodo tendono a verificarsi sullo stesso tipo di oggetto. Una spiegazione approfondita del caching inline può essere trovata qui.

    Stiamo andando a toccare il concetto generale di cache inline (nel caso in cui non si ha il tempo di passare attraverso la spiegazione approfondita di cui sopra).

    Quindi come funziona?, V8 mantiene una cache del tipo di oggetti che sono stati passati come parametro nelle chiamate di metodo recenti e utilizza queste informazioni per fare un’ipotesi sul tipo di oggetto che verrà passato come parametro in futuro. Se V8 è in grado di fare una buona ipotesi sul tipo di oggetto che verrà passato a un metodo, può ignorare il processo di capire come accedere alle proprietà dell’oggetto e, invece, utilizzare le informazioni memorizzate dalle ricerche precedenti alla classe nascosta dell’oggetto.

    Quindi, come sono correlati i concetti di classi nascoste e caching inline?, Ogni volta che un metodo viene chiamato su un oggetto specifico, il motore V8 deve eseguire una ricerca alla classe nascosta di quell’oggetto per determinare l’offset per l’accesso a una proprietà specifica. Dopo due chiamate riuscite dello stesso metodo alla stessa classe nascosta, V8 omette la ricerca della classe nascosta e aggiunge semplicemente l’offset della proprietà al puntatore dell’oggetto stesso. Per tutte le chiamate future di quel metodo, il motore V8 presuppone che la classe nascosta non sia cambiata e salta direttamente nell’indirizzo di memoria per una proprietà specifica utilizzando gli offset memorizzati dalle ricerche precedenti., Ciò aumenta notevolmente la velocità di esecuzione.

    Il caching inline è anche il motivo per cui è così importante che oggetti dello stesso tipo condividano classi nascoste. Se si creano due oggetti dello stesso tipo e con diverse classi nascoste (come abbiamo fatto nell’esempio precedente), V8 non sarà in grado di utilizzare la cache in linea perché anche se i due oggetti sono dello stesso tipo, le loro classi nascoste corrispondenti assegnano offset diversi alle loro proprietà.,

    I due oggetti sono fondamentalmente la stessa, ma la “a” e “b” proprietà sono stati creati in ordine diverso.

    Compilazione del codice macchina

    Una volta ottimizzato il grafico dell’idrogeno, l’albero motore lo abbassa a una rappresentazione di livello inferiore chiamata Litio. La maggior parte dell’implementazione del litio è specifica per l’architettura. L’allocazione del registro avviene a questo livello.,

    Alla fine, il litio viene compilato in codice macchina. Poi succede qualcos’altro chiamato OSR: sostituzione on-stack. Prima di iniziare a compilare e ottimizzare un metodo ovviamente di lunga durata, probabilmente lo stavamo eseguendo. V8 non ha intenzione di dimenticare quello che solo lentamente eseguito per ricominciare con la versione ottimizzata. Invece, trasformerà tutto il contesto che abbiamo (stack, registri) in modo da poter passare alla versione ottimizzata nel mezzo dell’esecuzione. Questo è un compito molto complesso, tenendo presente che, tra le altre ottimizzazioni, V8 ha inizialmente inserito il codice., V8 non è l’unico motore in grado di farlo.

    Esistono garanzie chiamate deoptimizzazione per effettuare la trasformazione opposta e tornare al codice non ottimizzato nel caso in cui un’ipotesi fatta dal motore non sia più valida.

    Garbage collection

    Per la garbage collection, V8 utilizza un approccio generazionale tradizionale di mark-and-sweep per pulire la vecchia generazione. La fase di marcatura dovrebbe interrompere l’esecuzione di JavaScript., Per controllare i costi GC e rendere l’esecuzione più stabile, V8 utilizza la marcatura incrementale: invece di percorrere l’intero heap, cercando di contrassegnare ogni oggetto possibile, percorre solo una parte dell’heap, quindi riprende l’esecuzione normale. La prossima fermata GC continuerà da dove si è fermata la passeggiata heap precedente. Ciò consente pause molto brevi durante la normale esecuzione. Come accennato in precedenza, la fase di sweep è gestita da thread separati.

    Accensione e TurboFan

    Con il rilascio di V8 5.9 all’inizio del 2017, è stata introdotta una nuova pipeline di esecuzione., Questa nuova pipeline ottiene miglioramenti delle prestazioni ancora più grandi e significativi risparmi di memoria nelle applicazioni JavaScript del mondo reale.

    La nuova pipeline di esecuzione è costruita su Ignition, l’interprete di V8 e TurboFan, il più recente compilatore di ottimizzazione di V8.

    Puoi controllare il post del blog del team V8 sull’argomento qui.

    Dalla versione 5.,9 di V8 è venuto fuori, full-codegen e albero motore (le tecnologie che hanno servito V8 dal 2010) non sono più stati utilizzati da V8 per l’esecuzione di JavaScript come il team V8 ha lottato per tenere il passo con le nuove funzionalità del linguaggio JavaScript e le ottimizzazioni necessarie per queste caratteristiche.

    Ciò significa che nel complesso V8 avrà un’architettura molto più semplice e manutenibile in futuro.

    Miglioramenti sul Web e Nodo.,js benchmark

    Questi miglioramenti sono solo l’inizio. La nuova pipeline di accensione e TurboFan apre la strada a ulteriori ottimizzazioni che aumenteranno le prestazioni JavaScript e ridurranno l’impronta di V8 sia in Chrome che in Node.js nei prossimi anni.

    Infine, ecco alcuni suggerimenti e trucchi su come scrivere JavaScript ben ottimizzato e migliore., Puoi facilmente derivare questi dal contenuto sopra, tuttavia, ecco un riepilogo per tua comodità:

    Come scrivere JavaScript ottimizzato

    1. Ordine delle proprietà dell’oggetto: crea sempre un’istanza delle proprietà dell’oggetto nello stesso ordine in modo che le classi nascoste e successivamente il codice ottimizzato possano essere condivisi.
    2. Proprietà dinamiche: l’aggiunta di proprietà a un oggetto dopo l’istanziazione forzerà la modifica di una classe nascosta e rallenterà tutti i metodi ottimizzati per la classe nascosta precedente. Invece, assegnare tutte le proprietà di un oggetto nel suo costruttore.,
    3. Metodi: il codice che esegue ripetutamente lo stesso metodo verrà eseguito più velocemente del codice che esegue molti metodi diversi solo una volta (a causa del caching inline).
    4. Array: evita array sparsi dove le chiavi non sono numeri incrementali. Gli array sparsi che non hanno tutti gli elementi al loro interno sono una tabella hash. Gli elementi in tali array sono più costosi da accedere. Inoltre, cerca di evitare di pre-allocare array di grandi dimensioni. È meglio crescere mentre vai. Infine, non eliminare gli elementi negli array. Rende le chiavi sparse.
    5. Valori con tag: V8 rappresenta oggetti e numeri con 32 bit., Usa un bit per sapere se è un oggetto (flag = 1) o un intero (flag = 0) chiamato SMI (SMall Integer) a causa dei suoi 31 bit. Quindi, se un valore numerico è maggiore di 31 bit, V8 inscatolerà il numero, trasformandolo in un doppio e creando un nuovo oggetto per inserire il numero all’interno. Provare a utilizzare numeri firmati a 31 bit quando possibile per evitare la costosa operazione di boxe in un oggetto JS.

    Noi di SessionStack cerchiamo di seguire queste best practice nella scrittura di codice JavaScript altamente ottimizzato., Il motivo è che una volta integrato SessionStack nella tua app Web di produzione, inizia a registrare tutto: tutte le modifiche DOM, le interazioni utente, le eccezioni JavaScript, le tracce di stack, le richieste di rete fallite e i messaggi di debug.
    Con SessionStack, puoi riprodurre i problemi nelle tue app Web come video e vedere tutto ciò che è successo al tuo utente. E tutto questo deve accadere senza alcun impatto sulle prestazioni per la tua app web.
    C’è un piano gratuito che ti permette di iniziare gratuitamente.

    Share

    Lascia un commento

    Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *