Statischer Arbeitsspeicher in JavaScript mit Objektpools

Colt McAnlis
Colt McAnlis

Einführung

Sie erhalten eine E-Mail, in der Sie darüber informiert werden, dass Ihr Onlinespiel oder Ihre Web-App nach einer gewissen Zeit nicht mehr richtig funktioniert. Sie durchsuchen Ihren Code, finden aber nichts Auffälliges, bis Sie die Tools zur Speicherleistung von Chrome öffnen und Folgendes sehen:

Ein Schnappschuss aus deiner Erinnerungszeitachse

Einer Ihrer Kollegen lacht, weil er erkennt, dass Sie ein Leistungsproblem im Zusammenhang mit dem Arbeitsspeicher haben.

In der Diagrammansicht für den Arbeitsspeicher ist dieses Sägezahnmuster ein deutlicher Hinweis auf ein potenziell kritisches Leistungsproblem. Wenn die Speichernutzung steigt, wird auch der Diagrammbereich in der Zeitachse größer. Wenn das Diagramm plötzlich abfällt, ist das ein Fall, in dem der Garbage Collector ausgeführt wurde und die referenzierten Speicherobjekte bereinigt hat.

Bedeutung der Sägezähne

In einem Diagramm wie diesem sehen Sie, dass viele Garbage-Collection-Ereignisse auftreten, was die Leistung Ihrer Web-Apps beeinträchtigen kann. In diesem Artikel erfahren Sie, wie Sie die Speichernutzung steuern und die Auswirkungen auf die Leistung reduzieren können.

Automatische Speicherbereinigung und Leistungskosten

Das Speichermodell von JavaScript basiert auf einer Technologie, die als Garbage Collector bezeichnet wird. In vielen Sprachen ist der Programmierer direkt dafür verantwortlich, Speicher aus dem Memory Heap des Systems zuzuweisen und freizugeben. Ein Garbage Collector-System übernimmt diese Aufgabe jedoch für den Programmierer. Das bedeutet, dass Objekte nicht direkt aus dem Speicher freigegeben werden, wenn der Programmierer sie dereferenziert, sondern zu einem späteren Zeitpunkt, wenn die Heuristik des GC entscheidet, dass dies vorteilhaft wäre. Für diesen Entscheidungsprozess muss der GC eine statistische Analyse der aktiven und inaktiven Objekte durchführen, was einige Zeit in Anspruch nimmt.

Die automatische Speicherbereinigung wird oft als das Gegenteil der manuellen Speicherverwaltung dargestellt, bei der der Programmierer angeben muss, welche Objekte freigegeben und an das Speichersystem zurückgegeben werden sollen.

Der Prozess, bei dem ein GC Speicher freigibt, ist nicht kostenlos. Er beansprucht in der Regel einen Teil der verfügbaren Leistung, da er Zeit für seine Arbeit benötigt. Außerdem entscheidet das System selbst, wann er ausgeführt wird. Sie haben keine Kontrolle über diese Aktion. Ein GC-Impuls kann jederzeit während der Codeausführung auftreten und die Codeausführung blockieren, bis er abgeschlossen ist. Die Dauer dieses Impulses ist Ihnen im Allgemeinen nicht bekannt. Die Ausführung dauert je nachdem, wie Ihr Programm den Arbeitsspeicher zu einem bestimmten Zeitpunkt nutzt, eine gewisse Zeit.

Leistungsstarke Anwendungen sind auf konsistente Leistungsgrenzen angewiesen, um eine reibungslose Nutzung zu ermöglichen. Garbage-Collector-Systeme können dieses Ziel beeinträchtigen, da sie zu zufälligen Zeiten und für zufällige Zeiträume ausgeführt werden können. Dadurch wird die verfügbare Zeit, die die Anwendung benötigt, um ihre Leistungsziele zu erreichen, verkürzt.

Memory Churn reduzieren, Garbage-Collection-Steuern reduzieren

Wie bereits erwähnt, wird ein GC-Impuls ausgelöst, wenn eine Reihe von Heuristiken feststellt, dass genügend inaktive Objekte vorhanden sind, sodass ein Impuls vorteilhaft wäre. Der Schlüssel zur Reduzierung der Zeit, die der Garbage Collector für Ihre Anwendung benötigt, liegt also darin, so viele Fälle von übermäßiger Objekterstellung und -freigabe wie möglich zu vermeiden. Dieser Prozess des häufigen Erstellens/Freigebens von Objekten wird als „Memory Churn“ bezeichnet. Wenn Sie den Memory Churn während der Lebensdauer Ihrer Anwendung reduzieren können, verringern Sie auch die Zeit, die die automatische Speicherbereinigung für die Ausführung benötigt. Das bedeutet, dass Sie die Anzahl der erstellten und zerstörten Objekte reduzieren oder ganz entfernen müssen. Sie müssen also die Speicherzuweisung beenden.

Dadurch wird Ihr Memory Graph von diesem Zustand :

Ein Schnappschuss aus deiner Erinnerungszeitachse

zu diesem:

Statischer Arbeitsspeicher (JavaScript)

In diesem Modell sehen Sie, dass das Diagramm kein Sägezahnmuster mehr hat, sondern am Anfang stark ansteigt und dann im Laufe der Zeit langsam zunimmt. Wenn Sie aufgrund von Memory Churn Leistungsprobleme haben, sollten Sie diese Art von Diagramm erstellen.

Umstellung auf JavaScript mit statischem Speicher

Statischer Arbeitsspeicher in JavaScript ist eine Technik, bei der zu Beginn der App der gesamte Arbeitsspeicher, der für die Lebensdauer der App benötigt wird, vorab zugewiesen wird. Dieser Arbeitsspeicher wird dann während der Ausführung verwaltet, wenn Objekte nicht mehr benötigt werden. Wir können dieses Ziel in wenigen einfachen Schritten erreichen:

  1. Instrumentieren Sie Ihre Anwendung, um die maximale Anzahl der erforderlichen Live-Speicherobjekte (pro Typ) für eine Reihe von Nutzungsszenarien zu ermitteln.
  2. Implementieren Sie Ihren Code neu, um diesen maximalen Betrag vorab zuzuweisen, und rufen Sie ihn dann manuell ab bzw. geben Sie ihn frei, anstatt auf den Hauptspeicher zuzugreifen.

In der Realität müssen wir für Punkt 1 auch ein wenig von Punkt 2 tun. Beginnen wir also damit.

Objektpool

Einfach ausgedrückt ist Objekt-Pooling der Prozess, bei dem eine Reihe nicht verwendeter Objekte desselben Typs beibehalten wird. Wenn Sie ein neues Objekt für Ihren Code benötigen, weisen Sie nicht ein neues Objekt aus dem Memory Heap des Systems zu, sondern verwenden eines der ungenutzten Objekte aus dem Pool. Wenn der externe Code das Objekt nicht mehr benötigt, wird es nicht im Hauptspeicher freigegeben, sondern an den Pool zurückgegeben. Da das Objekt nie aus dem Code dereferenziert (also gelöscht) wird, wird es nicht von der automatischen Speicherbereinigung erfasst. Durch die Verwendung von Objektpools hat der Programmierer die Kontrolle über den Speicher und der Einfluss des Garbage Collectors auf die Leistung wird reduziert.

Da eine Anwendung eine heterogene Gruppe von Objekttypen verwaltet, ist für die ordnungsgemäße Verwendung von Objektpools ein Pool pro Typ erforderlich, der während der Laufzeit der Anwendung eine hohe Fluktuation aufweist.

var newEntity = gEntityObjectPool.allocate();
newEntity.pos = {x: 215, y: 88};

//..... do some stuff with the object that we need to do

gEntityObjectPool.free(newEntity); //free the object when we're done
newEntity = null; //free this object reference

Bei den meisten Anwendungen wird irgendwann ein Punkt erreicht, an dem keine neuen Objekte mehr zugewiesen werden müssen. Wenn Sie Ihre Anwendung mehrmals ausführen, sollten Sie ein gutes Gefühl dafür bekommen, wie hoch diese Obergrenze ist, und können diese Anzahl von Objekten beim Start Ihrer Anwendung vorab zuweisen.

Objekte vorab zuweisen

Wenn Sie Objekt-Pooling in Ihr Projekt implementieren, erhalten Sie einen theoretischen Höchstwert für die Anzahl der Objekte, die während der Laufzeit Ihrer Anwendung erforderlich sind. Wenn Sie Ihre Website verschiedenen Testszenarien unterzogen haben, können Sie sich ein gutes Bild von den Speicheranforderungen machen. Sie können diese Daten irgendwo katalogisieren und analysieren, um die Obergrenzen der Speicheranforderungen für Ihre Anwendung zu ermitteln.

In der veröffentlichten Version Ihrer App können Sie dann die Initialisierungsphase so festlegen, dass alle Objektpools bis zu einer bestimmten Menge vorausgefüllt werden. Dadurch wird die gesamte Objektinitialisierung an den Anfang der App verschoben und die Anzahl der dynamischen Zuweisungen während der Ausführung reduziert.

function init() {
  //preallocate all our pools. 
  //Note that we keep each pool homogeneous wrt object types
  gEntityObjectPool.preAllocate(256);
  gDomObjectPool.preAllocate(888);
}

Die gewählte Menge hängt stark vom Verhalten Ihrer Anwendung ab. Manchmal ist das theoretische Maximum nicht die beste Option. Wenn Sie beispielsweise den durchschnittlichen Höchstwert auswählen, kann dies bei Nicht-Powerusern zu einem geringeren Speicherbedarf führen.

Keine Wunderwaffe

Es gibt eine ganze Reihe von Apps, bei denen statische Speicherwachstumsmuster von Vorteil sein können. Wie Chrome DevRel Renato Mangini jedoch betont, gibt es einige Nachteile.

Fazit

Einer der Gründe, warum JavaScript ideal für das Web ist, ist, dass es eine schnelle, unterhaltsame und einfache Sprache für den Einstieg ist. Das liegt vor allem daran, dass es nur wenige Syntaxbeschränkungen gibt und Speicherprobleme für Sie gelöst werden. Sie können sich auf das Programmieren konzentrieren und die lästige Arbeit dem Tool überlassen. Bei leistungsstarken Webanwendungen wie HTML5-Spielen kann die automatische Speicherbereinigung jedoch oft die dringend benötigte Framerate beeinträchtigen und so die Nutzerfreundlichkeit für den Endnutzer verringern. Mit einer sorgfältigen Instrumentierung und der Verwendung von Objektpools können Sie diese Belastung der Framerate reduzieren und die Zeit für andere Dinge nutzen.

Quellcode

Im Web gibt es viele Implementierungen von Objektpools, daher möchte ich Sie nicht mit einer weiteren langweilen. Stattdessen verweise ich Sie auf diese, die jeweils spezifische Implementierungsnuancen haben. Das ist wichtig, da jede Anwendungsnutzung spezifische Implementierungsanforderungen haben kann.

Verweise