it-swarm.com.de

Best Practice für das Erstellen von Millionen von temporären Objekten

Was sind die "Best Practices" für das Erstellen (und Freigeben) von Millionen von kleinen Objekten?

Ich schreibe ein Schachprogramm in Java, und der Suchalgorithmus generiert für jede mögliche Bewegung ein einziges "Move" -Objekt. Bei einer nominalen Suche können leicht über eine Million Move-Objekte pro Sekunde generiert werden. Die JVM GC hat die Last meines Entwicklungssystems bewältigen können, aber ich bin an alternativen Ansätzen interessiert, die Folgendes ermöglichen:

  1. Minimieren Sie den Aufwand für die Garbage Collection und 
  2. reduzieren Sie den Hauptspeicherbedarf für Systeme der unteren Preisklasse. 

Eine große Mehrheit der Objekte ist sehr kurzlebig, aber etwa 1% der erzeugten Bewegungen werden als persistenter Wert beibehalten und zurückgegeben. Daher muss jede Pooling- oder Caching-Methode die Möglichkeit bieten, bestimmte Objekte von der Wiederverwendung auszuschließen . 

Ich erwarte keinen ausführlichen Beispielcode, würde mich aber über Anregungen für weiterführende Lektüre/Recherche oder Open Source-Beispiele ähnlicher Art freuen.

108

Führen Sie die Anwendung mit ausführlicher Speicherbereinigung aus:

Java -verbose:gc

Und es wird Ihnen sagen, wenn es sammelt. Es gibt zwei Arten von Sweeps, einen schnellen und einen vollen Sweep.

[GC 325407K->83000K(776768K), 0.2300771 secs]
[GC 325816K->83372K(776768K), 0.2454258 secs]
[Full GC 267628K->83769K(776768K), 1.8479984 secs]

Der Pfeil ist vor und nach der Größe.

Solange es nur GC und keine vollständige GC ist, sind Sie zu Hause sicher. Der reguläre GC ist ein Kopiensammler in der "jungen Generation", sodass Objekte, auf die nicht mehr verwiesen wird, einfach vergessen werden.

Lesen Java SE 6 - HotSpot-Garbage Collection-Sammlung von virtuellen Maschinen ist wahrscheinlich hilfreich. 

47

Seit Version 6 verwendet der Servermodus von JVM eine Escape-Analyse Technik. Wenn Sie es verwenden, können Sie alle GC vermeiden.

21
Mikhail

Nun, hier gibt es mehrere Fragen!

1 - Wie werden kurzlebige Objekte verwaltet?

Wie bereits erwähnt, kann die JVM mit einer großen Anzahl kurzlebiger Objekte perfekt umgehen, da sie der Weak Generational Hypothesis folgt.

Beachten Sie, dass wir von Objekten sprechen, die den Hauptspeicher (Heap) erreicht haben. Dies ist nicht immer der Fall. Viele Objekte, die Sie erstellen, verlassen nicht einmal ein CPU-Register. Betrachten Sie beispielsweise diese for-Schleife

for(int i=0, i<max, i++) {
  // stuff that implies i
}

Denken wir nicht an das Loop-Rolling (eine Optimierung, die die JVM stark in Ihrem Code durchführt). Wenn max gleich Integer.MAX_VALUE ist, kann die Ausführung der Schleife einige Zeit in Anspruch nehmen. Die Variable i wird jedoch niemals aus dem Schleifenblock entkommen. Daher setzt die JVM diese Variable in ein CPU-Register und erhöht sie regelmäßig, sendet sie jedoch nie zurück in den Hauptspeicher.

Das Erstellen von Millionen von Objekten ist also keine große Sache, wenn sie nur lokal verwendet werden. Sie werden tot sein, bevor sie in Eden aufbewahrt werden, so dass der GC sie nicht einmal bemerkt.

2 - Ist es sinnvoll, den Overhead der GC zu reduzieren?

Wie üblich kommt es darauf an.

Zunächst sollten Sie die GC-Protokollierung aktivieren, um eine klare Übersicht über die Vorgänge zu erhalten. Sie können es mit -Xloggc:gc.log -XX:+PrintGCDetails aktivieren.

Wenn Ihre Anwendung viel Zeit in einem GC-Zyklus verbringt, passen Sie den GC an. Andernfalls lohnt es sich möglicherweise nicht wirklich.

Wenn Sie beispielsweise alle 100 ms eine junge GC haben, die 10 ms dauert, verbringen Sie 10% Ihrer Zeit in der GC, und Sie haben 10 Sammlungen pro Sekunde (was huuuuuge ist). In einem solchen Fall würde ich keine Zeit mit dem GC-Tuning verbringen, da diese 10 GC/s noch vorhanden wären.

3 - Einige Erfahrungen

Ich hatte ein ähnliches Problem mit einer Anwendung, die eine große Menge einer bestimmten Klasse erstellte. In den GC-Protokollen bemerkte ich, dass die Erstellungsrate der Anwendung bei 3 GB/s lag, was viel zu viel ist (komm ... 3 Gigabyte Daten pro Sekunde?!).

Das Problem: Zu viele häufige GCs wurden durch zu viele Objekte verursacht.

In meinem Fall habe ich einen Speicher-Profiler angehängt und festgestellt, dass eine Klasse einen großen Prozentsatz aller meiner Objekte darstellt. Ich habe die Instantiierungen ausfindig gemacht, um herauszufinden, dass es sich bei dieser Klasse im Wesentlichen um ein Paar Boolesche Objekte handelt, die in einem Objekt eingeschlossen sind. In diesem Fall standen zwei Lösungen zur Verfügung:

  • Überarbeiten Sie den Algorithmus so, dass ich kein Paar Boolesche Werte zurückschicke, sondern ich habe stattdessen zwei Methoden, die jedes Boolean einzeln zurückgeben

  • Zwischenspeichern Sie die Objekte und wissen Sie, dass es nur 4 verschiedene Instanzen gab

Ich entschied mich für den zweiten, da er die Anwendung am wenigsten beeinflusste und leicht einzuführen war. Ich habe Minuten gebraucht, um eine Fabrik mit einem nicht-Thread-sicheren Cache zu installieren (ich brauchte keine Threadsicherheit, da ich irgendwann nur 4 verschiedene Instanzen hätte).

Die Zuteilungsrate sank auf 1 GB/s und auch die Häufigkeit junger GC (geteilt durch 3).

Hoffentlich hilft das !

18
Pierre Laporte

Wenn Sie nur Objekte wertschätzen (dh keine Referenzen auf andere Objekte) und wirklich, aber ich meine wirklich Tonnen und Tonnen davon, können Sie direkte ByteBuffers mit nativer Byte-Reihenfolge [die letztere ist wichtig] verwenden und Sie benötigen einige hundert Zeilen Code zuweisen/wiederverwenden + Getter/Setter. Getter sehen ähnlich aus wie long getQuantity(int tupleIndex){return buffer.getLong(tupleInex+QUANTITY_OFFSSET);} 

Das würde das GC-Problem fast vollständig lösen, solange Sie nur einmal zuweisen, das heißt, ein großes Stück und dann die Objekte selbst verwalten. Anstelle von Referenzen hätten Sie nur einen Index (dh int) in der ByteBuffer, die weitergegeben werden muss. Möglicherweise müssen Sie den Speicher auch selbst ausrichten.

Die Technik würde sich wie C and void* anfühlen, aber mit etwas Umhüllung ist es erträglich. Ein Nachteil für die Leistung könnte die Überprüfung von Grenzen sein, wenn der Compiler es nicht beseitigen kann. Ein großer Vorteil ist die Lokalität, wenn Sie die Tupel wie Vektoren verarbeiten, da der Objektheader fehlt, wird auch der Speicherbedarf reduziert.

Abgesehen davon ist es wahrscheinlich nicht erforderlich, dass Sie einen solchen Ansatz benötigen, da die junge Generation praktisch aller JVM trivial stirbt und die Allokationskosten nur ein Zeigefinger sind. Die Zuteilungskosten können etwas höher sein, wenn Sie final-Felder verwenden, da diese auf einigen Plattformen (vor allem ARM/Power) einen Speicherzaun erfordern. Auf x86 ist dies jedoch kostenlos.

11
bestsss

Wenn Sie davon ausgehen, dass GC ein Problem ist (da andere darauf hinweisen, dass dies möglicherweise nicht der Fall ist), werden Sie Ihr eigenes Speichermanagement für Ihren speziellen Fall implementieren, d. H. Eine Klasse, die unter massiver Abwanderung leidet. Versuchen Sie, das Pooling von Objekten zu starten. Ich habe Fälle gesehen, in denen es ziemlich gut funktioniert. Das Implementieren von Objektpools ist ein ausgereifter Pfad, daher müssen Sie hier nicht erneut besuchen. Achten Sie auf Folgendes:

  • multithreading: Die Verwendung von lokalen Thread-Pools kann für Ihren Fall funktionieren
  • backing-Datenstruktur: Verwenden Sie ArrayDeque, da es beim Entfernen eine gute Leistung bringt und keinen Zuweisungsaufwand verursacht
  • beschränke die Größe deines Pools :)

Messen Sie vorher/nachher etc

8
Nitsan Wakart

Ich habe ein ähnliches Problem getroffen. Versuchen Sie zunächst, die Größe der kleinen Objekte zu reduzieren. Wir haben einige Standardfeldwerte eingeführt, die sie in jeder Objektinstanz referenzieren.

Beispielsweise hat MouseEvent eine Referenz auf die Point-Klasse. Wir haben Punkte zwischengespeichert und sie referenziert, anstatt neue Instanzen zu erstellen. Dasselbe gilt beispielsweise für leere Zeichenfolgen.

Eine andere Quelle waren mehrere Booleans, die durch ein Int ersetzt wurden, und für jedes Boolean verwenden wir nur ein Byte des Int.

6
StanislavL

Ich habe dieses Szenario vor einiger Zeit mit etwas XML-Verarbeitungscode behandelt. Ich stellte fest, dass ich Millionen von XML-Tag-Objekten erstellte, die sehr klein waren (normalerweise nur eine Zeichenfolge) und extrem kurzlebig waren (Ausfall eines XPath - Checks bedeutete, dass keine Übereinstimmung gefunden wurde).

Ich habe einige ernsthafte Tests durchgeführt und kam zu dem Schluss, dass ich nur eine Verbesserung der Geschwindigkeit um etwa 7% erzielen konnte, indem ich eine Liste der verworfenen Tags verwende, anstatt neue zu erstellen. Nach der Implementierung stellte ich jedoch fest, dass für die freie Warteschlange ein Mechanismus erforderlich war, der zu Prune hinzugefügt wurde, wenn er zu groß wurde. Dadurch wurde meine Optimierung vollständig aufgehoben. Daher habe ich sie auf eine Option umgestellt.

Zusammenfassend - wahrscheinlich nicht wert -, aber ich bin froh zu sehen, dass Sie darüber nachdenken.

6
OldCurmudgeon

In Anbetracht der Tatsache, dass Sie ein Schachprogramm schreiben, gibt es einige spezielle Techniken, die Sie für anständige Leistungen verwenden können. Ein einfacher Ansatz besteht darin, ein großes Array von Longs (oder Bytes) zu erstellen und es als Stapel zu behandeln. Jedes Mal, wenn Ihr Bewegungsgenerator Züge erstellt, werden einige Zahlen auf den Stapel gelegt, z. bewegen Sie sich von Platz und bewegen Sie sich auf Platz. Wenn Sie den Suchbaum auswerten, werden Sie Moves abbrechen und eine Board-Darstellung aktualisieren.

Wenn Sie Ausdruckskraft haben möchten, verwenden Sie Objekte. Wenn Sie Geschwindigkeit wollen (in diesem Fall), gehen Sie nativ vor.

2
David Plumpton

Eine Lösung, die ich für solche Suchalgorithmen verwendet habe, besteht darin, nur ein Move-Objekt zu erstellen, es mit einer neuen Bewegung zu mutieren und die Bewegung rückgängig zu machen, bevor der Gültigkeitsbereich verlassen wird. Sie analysieren wahrscheinlich nur einen Zug nach dem anderen und speichern dann den besten Zug irgendwo. 

Wenn dies aus irgendeinem Grund nicht möglich ist und Sie die maximale Speicherauslastung reduzieren möchten, finden Sie hier einen guten Artikel zur Speichereffizienz: http://www.cs.virginia.edu/kim/publicity/pldi09tutorials/memory-efficient- Java-Tutorial.pdf

1
rkj

Objektpools bieten enorme (manchmal 10-fache) Verbesserungen bei der Objektzuordnung auf dem Heap. Die obige Implementierung mit einer verknüpften Liste ist jedoch sowohl naiv als auch falsch! Die verknüpfte Liste erstellt Objekte, um ihre interne Struktur zu verwalten, wodurch der Aufwand aufgehoben wird. Ein Ringpuffer, der ein Array von Objekten verwendet, funktioniert gut. In dem Beispiel give (ein Schachprogramm, das Züge verwaltet) sollte der Ringpuffer in ein Inhaberobjekt für die Liste aller berechneten Züge eingeschlossen werden. Es werden dann nur die Objektreferenzen der Moves-Inhaber weitergegeben.

0

Erstellen Sie einfach Ihre Millionen von Objekten und schreiben Sie Ihren Code auf die richtige Weise: Bewahren Sie keine unnötigen Verweise auf diese Objekte auf. GC erledigt den Dirty-Job für Sie. Sie können mit verbose GC herumspielen, um zu sehen, ob sie wirklich GC sind. Java IS zum Erstellen und Freigeben von Objekten. :)

0
gyorgyabraham

Ich denke, Sie sollten über die Stapelzuordnung in Java und die Escape-Analyse lesen. 

Wenn Sie sich eingehender mit diesem Thema befassen, stellen Sie möglicherweise fest, dass Ihre Objekte nicht einmal auf dem Heapspeicher zugeordnet sind und nicht von GC so erfasst werden, wie dies bei Objekten auf dem Heapspeicher der Fall ist. 

Es gibt eine Wikipedia-Erklärung zur Escape-Analyse mit Beispielen, wie dies in Java funktioniert:

http://en.wikipedia.org/wiki/Escape_analysis

0
spectre

Ich bin kein großer Fan von GC, deshalb versuche ich immer, Wege zu finden. In diesem Fall würde ich die Verwendung von Object Pool pattern vorschlagen:

Die Idee besteht darin, zu vermeiden, dass neue Objekte erstellt werden, indem sie in einem Stapel gespeichert werden, um sie später wieder verwenden zu können.

Class MyPool
{
   LinkedList<Objects> stack;

   Object getObject(); // takes from stack, if it's empty creates new one
   Object returnObject(); // adds to stack
}
0
Ilya Gazman