Best practice per ridurre l'attività di Garbage Collector in Javascript


94

Ho un'app Javascript abbastanza complessa, che ha un ciclo principale che viene chiamato 60 volte al secondo. Sembra che ci sia un sacco di garbage collection in corso (basato sull'output "a dente di sega" dalla sequenza temporale della memoria negli strumenti di sviluppo di Chrome) - e questo spesso influisce sulle prestazioni dell'applicazione.

Quindi, sto cercando di ricercare le migliori pratiche per ridurre la quantità di lavoro che deve fare il garbage collector. (La maggior parte delle informazioni che sono stato in grado di trovare sul Web riguarda l'evitare perdite di memoria, che è una domanda leggermente diversa: la mia memoria si sta liberando, è solo che c'è troppa raccolta di rifiuti in corso.) che questo si riduce principalmente al riutilizzo degli oggetti il ​​più possibile, ma ovviamente il diavolo è nei dettagli.

L'app è strutturata in "classi" sulla falsariga di Simple JavaScript Inheritance di John Resig .

Penso che un problema sia che alcune funzioni possono essere chiamate migliaia di volte al secondo (poiché vengono utilizzate centinaia di volte durante ogni iterazione del ciclo principale), e forse le variabili di lavoro locali in queste funzioni (stringhe, array, ecc.) potrebbe essere il problema.

Sono a conoscenza del raggruppamento di oggetti per oggetti più grandi / pesanti (e lo usiamo fino a un certo punto), ma sto cercando tecniche che possano essere applicate su tutta la linea, in particolare relative a funzioni che vengono chiamate molte volte in cicli stretti .

Quali tecniche posso utilizzare per ridurre la quantità di lavoro che deve svolgere il garbage collector?

E, forse anche, quali tecniche si possono impiegare per identificare quali oggetti vengono raccolti maggiormente dai rifiuti? (È una base di codice molto grande, quindi il confronto delle istantanee dell'heap non è stato molto fruttuoso)


2
Hai un esempio del tuo codice che potresti mostrarci? Sarà più facile rispondere alla domanda allora (ma anche potenzialmente meno generale, quindi non sono sicuro qui)
John Dvorak

2
Che ne dici di interrompere l'esecuzione di funzioni migliaia di volte al secondo? È davvero l'unico modo per affrontare questo problema? Questa domanda sembra un problema XY. Stai descrivendo X ma quello che stai veramente cercando è una soluzione a Y.
Travis J

2
@TravisJ: Lo esegue solo 60 volte al secondo, che è una frequenza di animazione abbastanza comune. Non chiede di fare meno lavoro, ma come farlo in modo più efficiente nella raccolta dei rifiuti.
Bergi

1
@Bergi - "alcune funzioni possono essere chiamate migliaia di volte al secondo". Questa è una volta al millisecondo (forse peggio!). Non è affatto comune. 60 volte al secondo non dovrebbe essere un problema. Questa domanda è eccessivamente vaga e produrrà solo opinioni o ipotesi.
Travis J

4
@TravisJ - Non è affatto raro nei framework di gioco.
UpTheCreek

Risposte:


127

Molte delle cose che devi fare per ridurre al minimo l'abbandono GC vanno contro ciò che è considerato JS idiomatico nella maggior parte degli altri scenari, quindi tieni presente il contesto quando giudichi il consiglio che do.

L'assegnazione avviene negli interpreti moderni in diversi luoghi:

  1. Quando crei un oggetto tramite newo tramite la sintassi letterale [...], o{} .
  2. Quando concatenate le stringhe.
  3. Quando si entra in un ambito che contiene dichiarazioni di funzione.
  4. Quando si esegue un'azione che attiva un'eccezione.
  5. Quando valuti un'espressione di funzione: (function (...) { ... }) .
  6. Quando esegui un'operazione che costringe a Object like Object(myNumber) oNumber.prototype.toString.call(42)
  7. Quando chiami un builtin che fa uno di questi sotto il cofano, come Array.prototype.slice .
  8. Quando usi arguments per riflettere sull'elenco dei parametri.
  9. Quando dividi una stringa o abbini un'espressione regolare.

Evita di farlo e raggruppa e riutilizza gli oggetti ove possibile.

In particolare, cerca opportunità per:

  1. Estrai le funzioni interne che non hanno o poche dipendenze dallo stato di chiusura in un ambito più alto e più longevo. (Alcuni minificatori di codice come il compilatore Closure possono incorporare funzioni interne e potrebbero migliorare le prestazioni del GC.)
  2. Evita di utilizzare stringhe per rappresentare dati strutturati o per l'indirizzamento dinamico. Evita in particolare di analizzare ripetutamente le corrispondenze using splito con espressioni regolari poiché ognuna richiede più allocazioni di oggetti. Ciò accade spesso con le chiavi nelle tabelle di ricerca e negli ID dei nodi DOM dinamici. Ad esempio, lookupTable['foo-' + x]ed document.getElementById('foo-' + x)entrambi implicano un'allocazione poiché c'è una concatenazione di stringhe. Spesso è possibile allegare chiavi a oggetti di lunga durata invece di ricatenarli. A seconda dei browser che devi supportare, potresti essere in grado di utilizzareMap di utilizzare gli oggetti direttamente come chiavi.
  3. Evita di rilevare eccezioni sui normali percorsi di codice. Invece di try { op(x) } catch (e) { ... }farlo if (!opCouldFailOn(x)) { op(x); } else { ... }.
  4. Quando non puoi evitare di creare stringhe, ad esempio per passare un messaggio a un server, usa un builtin like JSON.stringify che usa un buffer nativo interno per accumulare contenuto invece di allocare più oggetti.
  5. Evita di usare callback per eventi ad alta frequenza e, dove puoi, passa come callback una funzione di lunga durata (vedi 1) che ricrea lo stato dal contenuto del messaggio.
  6. Evita di usare le argumentsfunzioni dal che usano che devono creare un oggetto simile a un array quando vengono chiamate.

Ho suggerito di utilizzare JSON.stringifyper creare messaggi di rete in uscita. L'analisi dei messaggi di input utilizzando JSON.parseovviamente implica l'allocazione e molto per i messaggi di grandi dimensioni. Se puoi rappresentare i tuoi messaggi in arrivo come array di primitive, puoi salvare molte allocazioni. L'unico altro builtin attorno al quale è possibile costruire un parser che non alloca è String.prototype.charCodeAt. Tuttavia, un parser per un formato complesso che utilizza solo quello sarà infernale da leggere.


Non pensi che gli JSON.parseoggetti d allocino uno spazio minore (o uguale) della stringa del messaggio?
Bergi

@Bergi, Dipende dal fatto che i nomi delle proprietà richiedano allocazioni separate, ma un parser che genera eventi invece di un albero di analisi non esegue allocaitoni estranei.
Mike Samuel

Risposta fantastica, grazie! Molte scuse per la ricompensa in scadenza - Ero in viaggio in quel momento e per qualche motivo non sono riuscito ad accedere a SO con il mio account Gmail sul mio telefono ....: /
UpTheCreek

Per compensare il mio cattivo tempismo con la taglia, ne ho aggiunta un'altra per ricaricarla (200 era il minimo che potevo dare;) - Per qualche motivo, però, mi richiede di aspettare 24 ore prima di assegnarla (anche se Ho selezionato "ricompensa risposta esistente"). Sarà tuo domani ...
UpTheCreek

@UpTheCreek, non preoccuparti. Sono contento che tu l'abbia trovato utile.
Mike Samuel,

13

Gli strumenti per sviluppatori di Chrome hanno una funzione molto interessante per tracciare l'allocazione della memoria. Si chiama Memory Timeline. Questo articolo descrive alcuni dettagli. Immagino sia di questo che stai parlando del "dente di sega"? Questo è un comportamento normale per la maggior parte dei runtime con GC. L'allocazione procede finché non viene raggiunta una soglia di utilizzo che attiva una raccolta. Normalmente ci sono diversi tipi di raccolte a diverse soglie.

Cronologia della memoria in Chrome

Le raccolte di dati inutili sono incluse nell'elenco degli eventi associati alla traccia insieme alla loro durata. Sul mio notebook piuttosto vecchio, le raccolte effimere si verificano a circa 4 Mb e richiedono 30 ms. Questa è 2 delle tue iterazioni di loop a 60Hz. Se si tratta di un'animazione, le raccolte di 30 ms probabilmente stanno causando balbuzie. Dovresti iniziare qui per vedere cosa sta succedendo nel tuo ambiente: dove si trova la soglia di raccolta e quanto tempo impiegano le tue raccolte. Questo ti fornisce un punto di riferimento per valutare le ottimizzazioni. Ma probabilmente non farai di meglio che diminuire la frequenza della balbuzie rallentando la velocità di allocazione, allungando l'intervallo tra le raccolte.

Il passaggio successivo consiste nell'utilizzare i profili | Funzionalità Record Heap Allocations per generare un catalogo di allocazioni per tipo di record. Questo mostrerà rapidamente quali tipi di oggetti stanno consumando la maggior parte della memoria durante il periodo di traccia, che è equivalente alla velocità di allocazione. Concentrati su questi in ordine decrescente di frequenza.

Le tecniche non sono scienza missilistica. Evita gli oggetti in scatola quando puoi farlo con uno senza scatola. Usa variabili globali per contenere e riutilizzare singoli oggetti in box piuttosto che allocare nuovi oggetti in ogni iterazione. Raggruppa tipi di oggetti comuni in elenchi liberi invece di abbandonarli. Risultati della concatenazione di stringhe di cache che sono probabilmente riutilizzabili in iterazioni future. Evita l'allocazione solo per restituire i risultati della funzione impostando invece le variabili in un ambito di inclusione. Dovrai considerare ogni tipo di oggetto nel suo contesto per trovare la strategia migliore. Se hai bisogno di aiuto con le specifiche, pubblica una modifica che descriva i dettagli della sfida che stai esaminando.

Sconsiglio di pervertire il tuo normale stile di codifica in un'applicazione nel tentativo di produrre meno spazzatura. Questo è per lo stesso motivo per cui non dovresti ottimizzare la velocità prematuramente. La maggior parte dei tuoi sforzi e la maggior parte della complessità e dell'oscurità del codice saranno prive di significato.


Giusto, questo è quello che intendo per dente di sega. So che ci sarà sempre uno schema a dente di sega di qualche tipo, ma la mia preoccupazione è che con la mia app la frequenza a dente di sega e le "scogliere" siano piuttosto alte. È interessante notare che gli eventi GC non appaiono sul mio calendario - gli unici eventi che appaiono nel riquadro 'record' (quello centrale) sono: request animation frame, animation frame fired, e composite layers. Non ho idea del motivo per cui non vedo GC Eventcome te (questa è l'ultima versione di Chrome e anche canarino).
UpTheCreek

4
Ho provato a utilizzare il profiler con "allocazioni di heap di record" ma finora non l'ho trovato molto utile. Forse è perché non so come usarlo correttamente. Sembra essere pieno di riferimenti che non significano nulla per me, come @342342e code relocation info.
UpTheCreek

9

Come principio generale, dovresti memorizzare nella cache il più possibile e fare il minimo di creazione e distruzione per ogni esecuzione del tuo ciclo.

La prima cosa che mi viene in mente è ridurre l'uso di funzioni anonime (se ne avete) all'interno del vostro ciclo principale. Inoltre sarebbe facile cadere nella trappola di creare e distruggere oggetti che vengono passati ad altre funzioni. Non sono affatto un esperto di javascript, ma immagino che questo:

var options = {var1: value1, var2: value2, ChangingVariable: value3};
function loopfunc()
{
    //do something
}

while(true)
{
    $.each(listofthings, loopfunc);

    options.ChangingVariable = newvalue;
    someOtherFunction(options);
}

sarebbe molto più veloce di questo:

while(true)
{
    $.each(listofthings, function(){
        //do something on the list
    });

    someOtherFunction({
        var1: value1,
        var2: value2,
        ChangingVariable: newvalue
    });
}

C'è mai stato un periodo di inattività per il tuo programma? Forse hai bisogno che funzioni senza problemi per un secondo o due (ad esempio per un'animazione) e poi ha più tempo per l'elaborazione? Se questo è il caso, potrei vedere prendere oggetti che normalmente sarebbero raccolti in modo spazzatura durante l'animazione e mantenere un riferimento ad essi in qualche oggetto globale. Quindi, quando l'animazione finisce, puoi cancellare tutti i riferimenti e lasciare che il garbage collector faccia il suo lavoro.

Scusa se tutto questo è un po 'banale rispetto a quello che hai già provato e pensato.


Questo. Inoltre anche le funzioni menzionate all'interno di altre funzioni (che non sono IIFE) sono un abuso comune che brucia molta memoria ed è facile non vederlo.
Esailija

Grazie Chris! Sfortunatamente non ho tempi di inattività: /
UpTheCreek

4

Creerei uno o pochi oggetti nel global scope(dove sono sicuro che il garbage collector non è autorizzato a toccarli), quindi proverei a rifattorizzare la mia soluzione per utilizzare quegli oggetti per portare a termine il lavoro, invece di utilizzare variabili locali .

Ovviamente non può essere fatto ovunque nel codice, ma generalmente questo è il mio modo per evitare il garbage collector.

PS Potrebbe rendere quella parte specifica del codice un po 'meno gestibile.


Il GC elimina le mie variabili di ambito globale in modo coerente.
VectorVortec
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.