Best Practices zur Reduzierung der Garbage Collector-Aktivität in Javascript

94

Ich habe eine ziemlich komplexe Javascript-App, die eine Hauptschleife hat, die 60 Mal pro Sekunde aufgerufen wird. Es scheint eine Menge Speicherbereinigung im Gange zu sein (basierend auf der Sägezahnausgabe von der Speicherzeitleiste in den Chrome-Entwicklungstools) - und dies wirkt sich häufig auf die Leistung der Anwendung aus.

Daher versuche ich, nach Best Practices zu suchen, um den Arbeitsaufwand für den Garbage Collector zu reduzieren. (Die meisten Informationen, die ich im Internet finden konnte, beziehen sich auf die Vermeidung von Speicherlecks. Dies ist eine etwas andere Frage. Mein Speicher wird freigegeben, es wird nur zu viel Speicherplatz gesammelt.) Ich gehe davon aus dass es hauptsächlich darauf ankommt, Objekte so oft wie möglich wiederzuverwenden, aber natürlich steckt der Teufel im Detail.

Die App ist in 'Klassen' nach dem Vorbild von John Resigs Simple JavaScript Inheritance strukturiert .

Ich denke, ein Problem ist, dass einige Funktionen tausende Male pro Sekunde aufgerufen werden können (da sie bei jeder Iteration der Hauptschleife hunderte Male verwendet werden), und möglicherweise die lokalen Arbeitsvariablen in diesen Funktionen (Strings, Arrays usw.). könnte das Problem sein.

Ich bin mir des Objekt-Poolings für größere / schwerere Objekte bewusst (und wir verwenden dies bis zu einem gewissen Grad), aber ich suche nach Techniken, die auf der ganzen Linie angewendet werden können, insbesondere in Bezug auf Funktionen, die in engen Schleifen sehr oft aufgerufen werden .

Mit welchen Techniken kann ich den Arbeitsaufwand für den Garbage Collector reduzieren?

Und vielleicht auch - mit welchen Techniken kann ermittelt werden, welche Objekte am häufigsten im Müll gesammelt werden? (Es ist eine sehr große Codebasis, daher war der Vergleich von Schnappschüssen des Heaps nicht sehr fruchtbar.)

UpTheCreek
quelle
2
Haben Sie ein Beispiel für Ihren Code, das Sie uns zeigen könnten? Die Frage wird dann einfacher zu beantworten sein (aber möglicherweise auch weniger allgemein, daher bin ich mir hier nicht sicher)
John Dvorak
2
Wie wäre es, Funktionen tausendmal pro Sekunde zu stoppen? Ist das wirklich der einzige Weg, dies zu erreichen? Diese Frage scheint ein XY-Problem zu sein. Sie beschreiben X, aber was Sie wirklich suchen, ist eine Lösung für Y.
Travis J
2
@TravisJ: Er führt es nur 60 Mal pro Sekunde aus, was eine recht häufige Animationsrate ist. Er bittet nicht darum, weniger zu arbeiten, sondern darum, wie man die Müllabfuhr effizienter macht.
Bergi
1
@Bergi - "Einige Funktionen können tausende Male pro Sekunde aufgerufen werden". Das ist einmal pro Millisekunde (möglicherweise schlimmer!). Das ist überhaupt nicht üblich. 60 Mal pro Sekunde sollte kein Problem sein. Diese Frage ist zu vage und wird nur Meinungen oder Vermutungen hervorbringen.
Travis J
4
@TravisJ - Es ist in Spiel-Frameworks überhaupt nicht ungewöhnlich.
UpTheCreek

Antworten:

127

Viele der Dinge, die Sie tun müssen, um die GC-Abwanderung zu minimieren, widersprechen dem, was in den meisten anderen Szenarien als idiomatisch angesehen wird. Denken Sie also bitte an den Kontext, wenn Sie die Ratschläge beurteilen, die ich gebe.

Die Zuordnung erfolgt bei modernen Dolmetschern an mehreren Stellen:

  1. Wenn Sie ein Objekt über newoder über die Literal-Syntax erstellen [...], oder {}.
  2. Wenn Sie Zeichenfolgen verketten.
  3. Wenn Sie einen Bereich eingeben, der Funktionsdeklarationen enthält.
  4. Wenn Sie eine Aktion ausführen, die eine Ausnahme auslöst.
  5. Wenn Sie einen Funktionsausdruck auswerten : (function (...) { ... }).
  6. Wenn Sie eine Operation ausführen, die zu einem Objekt wie Object(myNumber)oder erzwingtNumber.prototype.toString.call(42)
  7. Wenn Sie ein eingebautes Gerät aufrufen, das eines dieser Funktionen unter der Haube ausführt, z Array.prototype.slice.
  8. Wenn Sie verwenden arguments, um über die Parameterliste zu reflektieren.
  9. Wenn Sie eine Zeichenfolge teilen oder mit einem regulären Ausdruck übereinstimmen.

Vermeiden Sie dies und bündeln und verwenden Sie Objekte nach Möglichkeit.

Achten Sie insbesondere auf folgende Möglichkeiten:

  1. Ziehen Sie innere Funktionen, die keine oder nur wenige Abhängigkeiten vom geschlossenen Zustand haben, in einen höheren, längerlebigen Bereich. (Einige Code-Minifier wie der Closure-Compiler können innere Funktionen integrieren und möglicherweise Ihre GC-Leistung verbessern.)
  2. Vermeiden Sie die Verwendung von Zeichenfolgen zur Darstellung strukturierter Daten oder zur dynamischen Adressierung. Vermeiden Sie insbesondere das wiederholte Parsen mit splitÜbereinstimmungen oder Übereinstimmungen mit regulären Ausdrücken, da für jede mehrere Objektzuweisungen erforderlich sind. Dies geschieht häufig mit Schlüsseln in Nachschlagetabellen und dynamischen DOM-Knoten-IDs. Zum Beispiel lookupTable['foo-' + x]und document.getElementById('foo-' + x)beide beinhalten eine Zuordnung, da es eine Zeichenfolgenverkettung gibt. Oft können Sie Schlüssel an langlebige Objekte anhängen, anstatt sie erneut zu verketten. Abhängig von den Browsern, die Sie unterstützen müssen, können Sie diese möglicherweise verwendenMap Objekte direkt als Schlüssel verwenden.
  3. Vermeiden Sie es, Ausnahmen auf normalen Codepfaden abzufangen. Statt zu try { op(x) } catch (e) { ... }tun if (!opCouldFailOn(x)) { op(x); } else { ... }.
  4. Wenn Sie das Erstellen von Zeichenfolgen nicht vermeiden können, z. B. um eine Nachricht an einen Server zu übergeben, verwenden Sie eine integrierte Funktion wie JSON.stringify die einen internen nativen Puffer verwendet, um Inhalte zu akkumulieren, anstatt mehrere Objekte .
  5. Vermeiden Sie die Verwendung von Rückrufen für hochfrequente Ereignisse. Übergeben Sie als Rückruf eine langlebige Funktion (siehe 1), die den Status aus dem Nachrichteninhalt neu erstellt.
  6. Vermeiden Sie die Verwendung von as- argumentsFunktionen, die beim Aufruf ein Array-ähnliches Objekt erstellen müssen.

Ich schlug JSON.stringifyvor, ausgehende Netzwerknachrichten zu erstellen. Das Parsen von Eingabenachrichten mit verwendet JSON.parsenatürlich eine Zuordnung, und viele davon für große Nachrichten. Wenn Sie Ihre eingehenden Nachrichten als Arrays von Grundelementen darstellen können, können Sie viele Zuordnungen speichern. Die einzige andere integrierte Funktion, um die Sie einen Parser erstellen können, der nicht zugeordnet ist, ist String.prototype.charCodeAt. Ein Parser für ein komplexes Format, das nur das verwendet, wird allerdings höllisch zu lesen sein.

Mike Samuel
quelle
Denken Sie nicht, dass die JSON.parsed-Objekte weniger (oder gleichen) Speicherplatz zuweisen als die Nachrichtenzeichenfolge?
Bergi
@Bergi, Das hängt davon ab, ob die Eigenschaftsnamen separate Zuordnungen erfordern, aber ein Parser, der Ereignisse anstelle eines Analysebaums generiert, führt keine fremden Zuordnungen durch.
Mike Samuel
Fantastische Antwort, danke! Viele Entschuldigungen für das abgelaufene Kopfgeld - ich war zu der Zeit unterwegs und aus irgendeinem Grund konnte ich mich mit meinem Google Mail-Konto auf meinem Telefon nicht bei SO anmelden ....: /
UpTheCreek
Um mein schlechtes Timing mit dem Kopfgeld auszugleichen, habe ich ein zusätzliches hinzugefügt, um es aufzufüllen (200 war das Minimum, das ich geben konnte;) - Aus irgendeinem Grund muss ich jedoch 24 Stunden warten, bevor ich es vergebe (obwohl) Ich habe 'Vorhandene Antwort belohnen' ausgewählt. Wird morgen dein sein ...
UpTheCreek
@ UpTheCreek, keine Sorge. Ich bin froh, dass Sie es nützlich fanden.
Mike Samuel
13

Die Chrome-Entwicklertools bieten eine sehr schöne Funktion zum Nachverfolgen der Speicherzuordnung. Es heißt Memory Timeline. Dieser Artikel beschreibt einige Details. Ich nehme an, das ist es, wovon du sprichst, was den "Sägezahn" betrifft? Dies ist für die meisten GC-Laufzeiten normal. Die Zuordnung wird fortgesetzt, bis ein Nutzungsschwellenwert erreicht ist, der eine Sammlung auslöst. Normalerweise gibt es verschiedene Arten von Sammlungen mit unterschiedlichen Schwellenwerten.

Speicherzeitleiste in Chrome

Garbage Collections werden zusammen mit ihrer Dauer in die dem Trace zugeordnete Ereignisliste aufgenommen. Auf meinem ziemlich alten Notizbuch treten kurzlebige Sammlungen bei etwa 4 MB auf und dauern 30 ms. Dies sind 2 Ihrer 60-Hz-Schleifeniterationen. Wenn es sich um eine Animation handelt, verursachen 30-ms-Sammlungen wahrscheinlich Stottern. Sie sollten hier beginnen, um zu sehen, was in Ihrer Umgebung vor sich geht: Wo liegt der Sammlungsschwellenwert und wie lange Ihre Sammlungen dauern. Dies gibt Ihnen einen Bezugspunkt zur Bewertung von Optimierungen. Aber Sie werden wahrscheinlich nichts Besseres tun, als die Häufigkeit des Stotterns zu verringern, indem Sie die Zuordnungsrate verlangsamen und das Intervall zwischen den Sammlungen verlängern.

Der nächste Schritt ist die Verwendung der Profile | Funktion "Datensatz-Heap-Zuordnungen" zum Generieren eines Katalogs von Zuordnungen nach Datensatztyp. Dadurch wird schnell angezeigt, welche Objekttypen während des Ablaufzeitraums den meisten Speicher belegen, was der Zuordnungsrate entspricht. Konzentrieren Sie sich in absteigender Reihenfolge auf diese.

Die Techniken sind keine Raketenwissenschaft. Vermeiden Sie Objekte in Boxen, wenn Sie mit Objekten in Boxen arbeiten können. Verwenden Sie globale Variablen, um Objekte mit einzelnen Boxen zu speichern und wiederzuverwenden, anstatt in jeder Iteration neue zuzuweisen. Bündeln Sie allgemeine Objekttypen in freien Listen, anstatt sie aufzugeben. Ergebnisse der Verkettung von Cache-Zeichenfolgen, die wahrscheinlich in zukünftigen Iterationen wiederverwendet werden können. Vermeiden Sie die Zuordnung, um nur Funktionsergebnisse zurückzugeben, indem Sie stattdessen Variablen in einem umschließenden Bereich festlegen. Sie müssen jeden Objekttyp in einem eigenen Kontext betrachten, um die beste Strategie zu finden. Wenn Sie Hilfe bei Einzelheiten benötigen, veröffentlichen Sie eine Bearbeitung, in der Details der Herausforderung beschrieben werden, die Sie sich ansehen.

Ich rate davon ab, Ihren normalen Codierungsstil während einer Anwendung zu verfälschen, wenn Sie versuchen, weniger Müll zu produzieren. Aus dem gleichen Grund sollten Sie die Geschwindigkeit nicht vorzeitig optimieren. Der größte Teil Ihrer Bemühungen sowie ein Großteil der zusätzlichen Komplexität und Unklarheit des Codes sind bedeutungslos.

Gen
quelle
Richtig, das meine ich mit dem Sägezahn. Ich weiß, dass es immer ein Sägezahnmuster geben wird, aber ich mache mir Sorgen, dass mit meiner App die Sägezahnfrequenz und die "Klippen" ziemlich hoch sind. Interessanterweise werden GC-Ereignisse nicht in meiner Zeitleiste angezeigt. Die einzigen Ereignisse, die im Bereich "Datensätze" (dem mittleren) angezeigt werden request animation frame, sind: ,, animation frame firedund composite layers. Ich habe keine Ahnung, warum ich nicht so sehe GC Eventwie Sie (dies ist auf der neuesten Version von Chrom und auch Kanarienvogel).
UpTheCreek
4
Ich habe versucht, den Profiler mit 'Record Heap Allocations' zu verwenden, fand ihn aber bisher nicht sehr nützlich. Vielleicht liegt das daran, dass ich nicht weiß, wie ich es richtig verwenden soll. Es scheint voller Referenzen zu sein, die mir nichts bedeuten, wie @342342und code relocation info.
UpTheCreek
9

Grundsätzlich möchten Sie so viel wie möglich zwischenspeichern und so wenig wie möglich für jeden Lauf Ihrer Schleife erstellen und zerstören.

Das erste, was mir in den Sinn kommt, ist, die Verwendung anonymer Funktionen (falls vorhanden) in Ihrer Hauptschleife zu reduzieren. Es wäre auch leicht, in die Falle zu tappen, Objekte zu erstellen und zu zerstören, die an andere Funktionen übergeben werden. Ich bin kein Javascript-Experte, aber ich würde mir vorstellen, dass dies:

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

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

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

würde viel schneller laufen als dies:

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

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

Gibt es jemals Ausfallzeiten für Ihr Programm? Vielleicht muss es ein oder zwei Sekunden lang reibungslos laufen (z. B. für eine Animation), und dann hat es mehr Zeit für die Verarbeitung? Wenn dies der Fall ist, könnte ich sehen, wie Objekte, die normalerweise während der gesamten Animation als Müll gesammelt werden, in einem globalen Objekt referenziert werden. Wenn die Animation endet, können Sie alle Referenzen löschen und den Garbage Collector seine Arbeit erledigen lassen.

Tut mir leid, wenn das alles etwas trivial ist im Vergleich zu dem, was Sie bereits versucht und gedacht haben.

Chris B.
quelle
Dies. Außerdem sind Funktionen, die in anderen Funktionen erwähnt werden (die keine IIFEs sind), ebenfalls häufiger Missbrauch, der viel Speicherplatz beansprucht und leicht zu übersehen ist.
Esailija
Danke Chris! Ich habe leider keine Ausfallzeit: /
UpTheCreek
4

Ich würde ein oder wenige Objekte im global scope(wo ich sicher bin, dass der Garbage Collector sie nicht berühren darf) erstellen und dann versuchen, meine Lösung so umzugestalten, dass diese Objekte verwendet werden, um die Arbeit zu erledigen, anstatt lokale Variablen zu verwenden .

Natürlich konnte es nicht überall im Code gemacht werden, aber im Allgemeinen vermeide ich so Garbage Collector.

PS Es könnte dazu führen, dass dieser bestimmte Teil des Codes etwas weniger wartbar ist.

Mahdi
quelle
Der GC nimmt meine globalen Bereichsvariablen konsistent heraus.
VectorVortec