Einführung
Sie erhalten eine E-Mail, in der steht, dass Ihr Webspiel oder Ihre Webanwendung nach einer bestimmten Zeit eine schlechte Leistung erbringt. Sie sehen sich den Code an, aber nichts sticht heraus. Dann öffnen Sie die Chrome-Tools zur Speicherleistung und sehen Folgendes:
Einer Ihrer Kollegen lacht, weil er erkennt, dass Sie ein speicherbezogenes Leistungsproblem haben.
In der Speichergrafik ist dieses Sägezahnmuster ein deutliches Zeichen für ein potenziell kritisches Leistungsproblem. Wenn die Speicherauslastung steigt, wird auch der Diagrammbereich in der Zeitachse größer. Wenn der Wert im Diagramm plötzlich sinkt, wurde der Garbage Collector ausgeführt und die referenzierten Arbeitsspeicherobjekte wurden bereinigt.
In einer solchen Grafik sehen Sie, dass viele Garbage Collection-Ereignisse auftreten, was sich negativ auf die Leistung Ihrer Webanwendungen auswirken kann. In diesem Artikel erfahren Sie, wie Sie die Speichernutzung steuern und die Auswirkungen auf die Leistung reduzieren können.
Kosten für automatische Speicherbereinigung und Leistung
Das Speichermodell von JavaScript basiert auf einer Technologie, die als Garbage Collector bezeichnet wird. In vielen Sprachen ist der Programmierer direkt für die Zuweisung und Freigabe von Arbeitsspeicher aus dem Memory Heap des Systems verantwortlich. Ein Garbage Collector-System übernimmt diese Aufgabe jedoch im Namen des Programmierers. Das bedeutet, dass Objekte nicht direkt aus dem Arbeitsspeicher freigegeben werden, wenn der Programmierer sie dereferenziert, sondern erst zu einem späteren Zeitpunkt, wenn die Heuristiken des GC entscheiden, dass dies von Vorteil wäre. Dieser Entscheidungsprozess erfordert, dass der GC einige statistische Analysen an aktiven und inaktiven Objekten durchführt, 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 die Zuweisung aufheben und an das Speichersystem zurückgeben sollen.
Der Prozess, bei dem ein GC Speicher zurückfordert, ist nicht kostenlos. Er beeinträchtigt in der Regel die verfügbare Leistung, da er einen bestimmten Zeitraum in Anspruch nimmt. Außerdem entscheidet das System selbst, wann es ausgeführt wird. Sie haben keine Kontrolle über diese Aktion. Während der Codeausführung kann jederzeit ein GC-Impuls auftreten, der die Codeausführung blockiert, bis sie abgeschlossen ist. Die Dauer dieses Pulses ist Ihnen in der Regel nicht bekannt. Die Ausführung dauert einige Zeit, je nachdem, wie Ihr Programm den Arbeitsspeicher zu einem bestimmten Zeitpunkt nutzt.
Hochleistungsanwendungen basieren auf konsistenten Leistungsgrenzen, um eine reibungslose Nutzung zu gewährleisten. Garbage-Collection-Systeme können dieses Ziel untergraben, da sie zufällig zufällige Zeiträume laufen und so die verfügbare Zeit verringern, die die Anwendung benötigt, um ihre Leistungsziele zu erreichen.
Weniger Arbeitsspeicherabwanderung, weniger Steuern für die automatische Speicherbereinigung
Wie bereits erwähnt, wird ein GC-Impuls ausgeführt, sobald eine Reihe von Heuristiken feststellt, dass es genügend inaktive Objekte gibt, für die ein Impuls vorteilhaft wäre. Um die Zeit zu reduzieren, die der Garbage Collector für Ihre Anwendung benötigt, sollten Sie daher so viele Fälle wie möglich von übermäßigem Objekterstellen und ‑freigeben vermeiden. Dieser Vorgang, bei dem Objekte häufig erstellt und freigegeben werden, 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 GC für die Ausführung benötigt. Das bedeutet, dass Sie die Anzahl der erstellten und zerstörten Objekte entfernen oder reduzieren müssen. Sie müssen also aufhören, Speicher zuzuweisen.
Dadurch wird das Arbeitsspeicherdiagramm an folgende Stelle verschoben:
zu
In diesem Modell sehen Sie, dass das Diagramm kein Sägezahnmuster mehr hat, sondern zu Beginn stark ansteigt und sich dann im Laufe der Zeit langsam erhöht. Wenn Sie aufgrund von Arbeitsspeicherauslastung Leistungsprobleme haben, sollten Sie diese Art von Grafik erstellen.
Richtung JavaScript mit statischem Speicher
Static Memory JavaScript ist ein Verfahren, bei dem zu Beginn Ihrer Anwendung der gesamte Arbeitsspeicher, der während der Lebensdauer benötigt wird, vorab zugewiesen und während der Ausführung verwaltet wird, wenn Objekte nicht mehr benötigt werden. Das lässt sich in wenigen einfachen Schritten erreichen:
- Instrumentieren Sie Ihre Anwendung, um die maximale Anzahl der erforderlichen Live-Arbeitsspeicherobjekte (pro Typ) für verschiedene Anwendungsfälle zu ermitteln.
- Implementieren Sie Ihren Code neu, um diesen maximalen Wert vorab zuzuweisen und dann manuell abzurufen/freizugeben, anstatt den Arbeitsspeicher zu verwenden.
In Wirklichkeit erfordert Punkt 1, dass wir ein wenig Punkt 2 tun. Fangen wir also damit an.
Objektpool
Einfach ausgedrückt ist Objekt-Pooling der Vorgang, bei dem eine Reihe nicht verwendeter Objekte desselben Typs beibehalten wird. Wenn Sie ein neues Objekt für Ihren Code benötigen, wird nicht ein neues Objekt aus dem Speicher-Heap des Systems zugewiesen, sondern eines der nicht verwendeten Objekte aus dem Pool wird wiederverwendet. Sobald der externe Code mit dem Objekt fertig ist, wird es nicht an den Hauptspeicher freigegeben, sondern an den Pool zurückgegeben. Da das Objekt nie aus dem Code dereferenziert (d. h. gelöscht) wird, wird es nicht durch die Garbage Collection entfernt. Durch die Verwendung von Objektpools liegt der Programmierer die Kontrolle über den Arbeitsspeicher wieder in der Hand des Programmierers, sodass der Einfluss der automatischen Speicherbereinigung auf die Leistung reduziert wird.
Da eine Anwendung eine heterogene Gruppe von Objekttypen verwaltet, benötigen Sie für die ordnungsgemäße Verwendung von Objektpools einen Pool pro Typ, der während der Laufzeit Ihrer Anwendung häufig gewechselt wird.
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 es irgendwann nicht mehr nötig sein, neue Objekte zuzuweisen. Nach mehreren Ausführungen Ihrer Anwendung sollten Sie ein gutes Gefühl dafür haben, wie hoch dieses Obergrenze ist, und diese Anzahl von Objekten zu Beginn Ihrer Anwendung vorab zuweisen können.
Objekte vorab zuweisen
Wenn Sie das Objekt-Pooling in Ihrem Projekt implementieren, erhalten Sie ein theoretisches Maximum für die Anzahl der Objekte, die während der Laufzeit Ihrer Anwendung erforderlich sind. Wenn Sie Ihre Website durch verschiedene Testszenarien getestet haben, erhalten Sie eine gute Vorstellung von den erforderlichen Speicheranforderungen und können diese Daten irgendwo katalogisieren und analysieren, um die Obergrenzen der Arbeitsspeicheranforderungen für Ihre Anwendung zu ermitteln.
In der Veröffentlichungsversion Ihrer App können Sie dann die Initialisierungsphase so festlegen, dass alle Objektpools mit einer bestimmten Anzahl vorab gefüllt werden. Dadurch wird die gesamte Objektinitialisierung an den Anfang Ihrer App verschoben und die Anzahl der Zuweisungen, die während der Ausführung dynamisch erfolgen, wird reduziert.
function init() {
//preallocate all our pools.
//Note that we keep each pool homogeneous wrt object types
gEntityObjectPool.preAllocate(256);
gDomObjectPool.preAllocate(888);
}
Die von Ihnen gewählte Menge hängt stark vom Verhalten Ihrer Anwendung ab. Manchmal ist das theoretische Maximum nicht die beste Option. Wenn Sie beispielsweise das durchschnittliche Maximum auswählen, kann der Speicherbedarf für Nicht-Poweruser geringer sein.
Keine Wunderwaffe
Es gibt eine ganze Klassifizierung von Apps, bei denen statische Speicherwachstumsmuster von Vorteil sein können. Wie Renato Mangini, ebenfalls Chrome DevRel, jedoch anmerkt, gibt es einige Nachteile.
Fazit
Der Grund dafür, dass JavaScript ideal für das Web ist, liegt unter anderem daran, dass es eine schnelle, unterhaltsame und einfache Sprache für den Einstieg ist. Das liegt vor allem an den geringen Syntaxeinschränkungen und der Verarbeitung von Speicherproblemen in Ihrem Namen. Sie können einfach programmieren und die Drecksarbeit von ihm erledigen lassen. Bei leistungsstarken Webanwendungen wie HTML5-Spielen kann der GC jedoch häufig die für die Nutzerfreundlichkeit erforderliche Framerate beeinträchtigen. Mithilfe einer sorgfältigen Instrumentierung und der Verwendung von Objektpools können Sie diese Belastung für die Framerate reduzieren und die gewonnene Zeit für andere Dinge nutzen.
Quellcode
Es gibt viele Implementierungen von Objektpools im Web, daher möchte ich Sie nicht mit einer weiteren langweilen. Stattdessen möchte ich Sie auf diese verweisen, die jeweils spezifische Implementierungsnuancen haben. Das ist wichtig, da für jede Anwendungsnutzung spezifische Implementierungsanforderungen gelten können.
- Objektpool von Gamecore.js
- Beej's Object Pools
- Super einfacher Objektpool von Emehrkay
- Spielorientierter Objektpool von Steven Lambert
- objectPool-Einrichtung der RenderEngine