Best Practice zum Erstellen von Millionen kleiner temporärer Objekte

109

Was sind die "Best Practices" zum Erstellen (und Freigeben) von Millionen kleiner Objekte?

Ich schreibe ein Schachprogramm in Java und der Suchalgorithmus generiert ein einzelnes "Verschieben" -Objekt für jede mögliche Bewegung, und eine nominelle Suche kann leicht über eine Million Bewegungsobjekte pro Sekunde generieren. Die JVM GC war in der Lage, die Belastung meines Entwicklungssystems zu bewältigen, aber ich bin daran interessiert, alternative Ansätze zu untersuchen, die:

  1. Minimieren Sie den Aufwand für die Speicherbereinigung und
  2. Reduzieren Sie den maximalen Speicherbedarf für Systeme der unteren Preisklasse.

Die überwiegende Mehrheit der Objekte ist sehr kurzlebig, aber etwa 1% der generierten Bewegungen werden beibehalten und als beibehaltener Wert zurückgegeben. Daher müsste jede Pooling- oder Caching-Technik die Möglichkeit bieten, bestimmte Objekte von der Wiederverwendung auszuschließen .

Ich erwarte keinen vollständig ausgearbeiteten Beispielcode, würde mich aber über Vorschläge für weitere Lektüre / Recherchen oder Open-Source-Beispiele ähnlicher Art freuen.

Bescheidener Programmierer
quelle
11
Wäre das Fliegengewichtsmuster für Ihren Fall geeignet? en.wikipedia.org/wiki/Flyweight_pattern
Roger Rowland
4
Müssen Sie es in ein Objekt einkapseln?
nhahtdh
1
Das Fliegengewichtsmuster ist nicht geeignet, da die Objekte keine wesentlichen gemeinsamen Daten gemeinsam haben. Die Kapselung der Daten in einem Objekt ist zu groß, um in ein Grundelement gepackt zu werden. Deshalb suche ich nach Alternativen zu POJOs.
Demütiger Programmierer

Antworten:

47

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

java -verbose:gc

Und es wird Ihnen sagen, wann es sammelt. Es würde zwei Arten von Sweeps geben, einen schnellen und einen vollständigen 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 steht vor und nach der Größe.

Solange es sich nur um GC und nicht um eine vollständige GC handelt, sind Sie sicher zu Hause. Der reguläre GC ist ein Kopiersammler in der "jungen Generation", sodass Objekte, auf die nicht mehr verwiesen wird, einfach vergessen werden. Genau das möchten Sie.

Das Lesen der Garbage Collection-Optimierung für Java SE 6 HotSpot Virtual Machine ist wahrscheinlich hilfreich.

Niels Bech Nielsen
quelle
Experimentieren Sie mit der Java-Heap-Größe, um einen Punkt zu finden, an dem eine vollständige Speicherbereinigung selten ist. In Java 7 ist der neue G1 GC in einigen Fällen schneller (und in anderen langsamer).
Michael Shopsin
21

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

Mikhail
quelle
1
Escape-Analyse enttäuscht oft, es lohnt sich zu überprüfen, ob die JVM herausgefunden hat, was Sie tun oder nicht.
Nitsan Wakart
2
Wenn Sie Erfahrung mit dieser Option haben: -XX: + PrintEscapeAnalysis und -XX: + PrintEliminateAllocations. Das wäre toll zu teilen. Weil ich es nicht tue, ehrlich gesagt.
Mikhail
Siehe stackoverflow.com/questions/9032519/… Sie müssen einen Debug-Build für JDK 7 erhalten. Ich gebe zu, dass ich das nicht getan habe, aber mit JDK 6 war es erfolgreich.
Nitsan Wakart
19

Nun, hier gibt es mehrere Fragen in einer!

1 - Wie werden kurzlebige Objekte verwaltet?

Wie bereits erwähnt, kann die JVM perfekt mit einer großen Menge kurzlebiger Objekte umgehen, da sie der schwachen Generationshypothese folgt .

Beachten Sie, dass es sich um Objekte handelt, die den Hauptspeicher (Heap) erreicht haben. Dies ist nicht immer der Fall. Viele von Ihnen erstellte Objekte hinterlassen nicht einmal ein CPU-Register. Betrachten Sie zum Beispiel diese for-Schleife

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

Denken wir nicht an das Abrollen von Schleifen (eine Optimierung, die die JVM stark an Ihrem Code ausführt). Wenn maxgleich ist Integer.MAX_VALUE, kann die Ausführung Ihrer Schleife einige Zeit dauern. Die iVariable wird jedoch niemals aus dem Schleifenblock entkommen. Daher legt die JVM diese Variable in einem CPU-Register ab, erhöht sie regelmäßig, sendet sie jedoch niemals an den Hauptspeicher zurück.

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 gelagert werden, sodass der GC sie nicht einmal bemerkt.

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

Wie immer 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 aktivieren -Xloggc:gc.log -XX:+PrintGCDetails.

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

Wenn Sie beispielsweise alle 100 ms einen jungen GC haben, der 10 ms benötigt, verbringen Sie 10% Ihrer Zeit im GC und Sie haben 10 Sammlungen pro Sekunde (was riesig ist). In einem solchen Fall würde ich keine Zeit mit GC-Tuning verbringen, da diese 10 GC / s immer 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 habe ich festgestellt, dass die Erstellungsrate der Anwendung etwa 3 GB / s betrug, was viel zu viel ist (komm schon ... 3 Gigabyte Daten pro Sekunde ?!).

Das Problem: Zu viele häufige GCs, die durch zu viele erstellte Objekte verursacht werden.

In meinem Fall habe ich einen Speicherprofiler angehängt und festgestellt, dass eine Klasse einen großen Prozentsatz aller meiner Objekte darstellt. Ich habe die Instanziierungen aufgespürt, um herauszufinden, dass es sich bei dieser Klasse im Grunde genommen um ein Paar Boolescher Werte handelt, die in ein Objekt eingewickelt sind. In diesem Fall standen zwei Lösungen zur Verfügung:

  • Überarbeiten Sie den Algorithmus so, dass ich kein Paar Boolescher Werte zurückgebe, sondern zwei Methoden, die jeden Booleschen Wert separat zurückgeben

  • Zwischenspeichern Sie die Objekte, da Sie wissen, dass es nur 4 verschiedene Instanzen gibt

Ich entschied mich für die zweite, da sie die Anwendung am wenigsten beeinträchtigte und leicht einzuführen war. Ich habe Minuten gebraucht, um eine Factory mit einem nicht threadsicheren Cache einzurichten (ich brauchte keine Thread-Sicherheit, da ich schließlich nur 4 verschiedene Instanzen haben würde).

Die Zuweisungsrate sank auf 1 GB / s, ebenso wie die Häufigkeit junger GC (geteilt durch 3).

Hoffentlich hilft das !

Pierre Laporte
quelle
11

Wenn Sie nur Wertobjekte haben (dh keine Verweise auf andere Objekte) und wirklich, aber ich meine wirklich Tonnen und Tonnen von ihnen, können Sie direkt ByteBuffersmit nativer Bytereihenfolge verwenden [letzteres ist wichtig] und Sie benötigen einige hundert Zeilen von Code zum Zuweisen / Wiederverwenden + Getter / Setter. Getter sehen ähnlich auslong 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, dh einen großen Teil, und dann die Objekte selbst verwalten. Anstelle von Referenzen hätten Sie nur einen Index (dh int) für den ByteBuffer, der weitergegeben werden muss. Möglicherweise müssen Sie den Speicher auch selbst ausrichten.

Die Technik würde sich wie eine Anwendung anfühlen C and void*, aber mit etwas Verpackung ist sie erträglich. Ein Leistungsnachteil könnte darin bestehen, zu überprüfen, ob der Compiler ihn nicht beseitigt. Ein großer Vorteil ist die Lokalität, wenn Sie die Tupel wie Vektoren verarbeiten. Das Fehlen des Objekt-Headers verringert auch den Speicherbedarf.

Abgesehen davon ist es wahrscheinlich, dass Sie keinen solchen Ansatz benötigen, da die junge Generation praktisch aller JVM-Unternehmen trivial stirbt und die Zuweisungskosten nur ein Hinweis sind. Die Zuordnungskosten können etwas höher sein, wenn Sie finalFelder verwenden, da diese auf einigen Plattformen (nämlich ARM / Power) einen Speicherzaun erfordern. Auf x86 ist dies jedoch kostenlos.

Bests
quelle
8

Angenommen, Sie stellen fest, dass GC ein Problem ist (wie andere darauf hinweisen, dass dies möglicherweise nicht der Fall ist), implementieren Sie Ihre eigene Speicherverwaltung für Ihren Sonderfall, dh eine Klasse, die unter massiver Abwanderung leidet. Probieren Sie das Pooling von Objekten aus. Ich habe Fälle gesehen, in denen es recht gut funktioniert. Das Implementieren von Objektpools ist ein ausgetretener Weg, sodass Sie hier nicht erneut nachsehen müssen. Achten Sie auf Folgendes:

  • Multithreading: Die Verwendung von lokalen Thread-Pools funktioniert möglicherweise für Ihren Fall
  • Hintergrunddatenstruktur: Verwenden Sie ArrayDeque, da es beim Entfernen eine gute Leistung erbringt und keinen Zuordnungsaufwand hat
  • Begrenzen Sie die Größe Ihres Pools :)

Vor / nach usw. messen usw.

Nitsan Wakart
quelle
6

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

Beispielsweise hat MouseEvent einen Verweis auf die Point-Klasse. Wir haben Punkte zwischengespeichert und auf sie verwiesen, anstatt neue Instanzen zu erstellen. Gleiches gilt beispielsweise für leere Zeichenfolgen.

Eine andere Quelle waren mehrere Boolesche Werte, die durch einen Int ersetzt wurden. Für jeden Booleschen Wert verwenden wir nur ein Byte des Int.

StanislavL
quelle
Nur aus Interesse: Was hat es Ihnen in Bezug auf die Leistung gebracht? Haben Sie Ihre Bewerbung vor und nach der Änderung profiliert und wenn ja, welche Ergebnisse wurden erzielt?
Axel
@Axel Die Objekte verbrauchen viel weniger Speicher, sodass GC nicht so oft aufgerufen wird. Auf jeden Fall haben wir unsere App profiliert, aber es gab sogar einen visuellen Effekt der verbesserten Geschwindigkeit.
StanislavL
6

Ich habe dieses Szenario vor einiger Zeit mit XML-Verarbeitungscode behandelt. Ich habe Millionen von XML-Tag-Objekten erstellt, die sehr klein (normalerweise nur eine Zeichenfolge) und extrem kurzlebig waren (ein Fehlschlagen einer XPath- Prüfung bedeutete, dass keine Übereinstimmung vorliegt, also verwerfen Sie sie).

Ich habe einige ernsthafte Tests durchgeführt und bin zu dem Schluss gekommen, dass ich mit einer Liste verworfener Tags nur eine Geschwindigkeitsverbesserung von etwa 7% erzielen konnte, anstatt neue zu erstellen. Nach der Implementierung stellte ich jedoch fest, dass für die freie Warteschlange ein Mechanismus zum Bereinigen erforderlich war, wenn sie zu groß wurde. Dadurch wurde meine Optimierung vollständig aufgehoben, sodass ich sie auf eine Option umstellte.

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

OldCurmudgeon
quelle
2

Da Sie ein Schachprogramm schreiben, gibt es einige spezielle Techniken, die Sie für eine anständige Leistung 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 Bewegungen erstellt, werden einige Zahlen auf den Stapel geschoben, z. B. von Quadrat zu Quadrat. Während Sie den Suchbaum auswerten, werden Sie Bewegungen ausführen und eine Board-Darstellung aktualisieren.

Wenn Sie Ausdruckskraft wünschen, verwenden Sie Objekte. Wenn Sie Geschwindigkeit wollen (in diesem Fall), gehen Sie nativ.

David Plumpton
quelle
1

Eine Lösung, die ich für solche Suchalgorithmen verwendet habe, besteht darin, nur ein Verschiebungsobjekt zu erstellen, es mit einer neuen Verschiebung zu mutieren und die Verschiebung dann rückgängig zu machen, bevor der Bereich verlassen wird. Sie analysieren wahrscheinlich jeweils nur einen Zug und speichern dann irgendwo den besten Zug.

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

rkj
quelle
Toter Link. Gibt es eine andere Quelle für diesen Artikel?
dnault
0

Erstellen Sie einfach Ihre Millionen von Objekten und schreiben Sie Ihren Code auf die richtige Weise: Behalten Sie keine unnötigen Verweise auf diese Objekte bei. GC erledigt den Drecksjob für Sie. Sie können wie erwähnt mit ausführlicher GC herumspielen, um zu sehen, ob sie wirklich GC-fähig sind. In Java geht es um das Erstellen und Freigeben von Objekten. :) :)

Gyorgyabraham
quelle
1
Tut mir leid, Kumpel, ich bin mit Ihrem Ansatz nicht einverstanden ... Wie bei jeder Programmiersprache geht es bei Java darum, ein Problem innerhalb seiner Grenzen zu lösen. Wenn das OP durch GC eingeschränkt wird, wie helfen Sie ihm?
Nitsan Wakart
1
Ich erzähle ihm, wie Java tatsächlich funktioniert. Wenn er der Situation, Millionen von temporären Objekten zu haben, nicht ausweichen kann, könnte der beste Rat sein, dass die temporäre Klasse leicht sein sollte und er sicherstellen muss, dass er die Referenzen so schnell wie möglich veröffentlicht, nicht mehr einen einzigen Schritt. Vermisse ich etwas
Gyorgyabraham
Java unterstützt das Erstellen von Müll und würde ihn für Sie bereinigen, so viel ist wahr. Wenn der OP der Erstellung von Objekten nicht ausweichen kann und mit der in GC verbrachten Zeit unzufrieden ist, ist dies ein trauriges Ende. Mein Einwand ist gegen die Empfehlung, die Sie machen, um mehr Arbeit für GC zu machen, weil das irgendwie richtig Java ist.
Nitsan Wakart
0

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

Wenn Sie sich eingehender mit diesem Thema befassen, werden Sie möglicherweise feststellen, dass Ihre Objekte nicht einmal auf dem Heap zugeordnet sind und von GC nicht so erfasst werden, wie Objekte auf dem Heap sind.

Es gibt eine Wikipedia-Erklärung zur Escape-Analyse mit einem Beispiel dafür, wie dies in Java funktioniert:

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

luke1985
quelle
0

Ich bin kein großer Fan von GC, deshalb versuche ich immer, Wege zu finden, um das zu umgehen. In diesem Fall würde ich die Verwendung des Objektpoolmusters vorschlagen :

Die Idee ist, das Erstellen neuer Objekte zu vermeiden, indem Sie sie in einem Stapel speichern, damit Sie sie später wiederverwenden können.

Class MyPool
{
   LinkedList<Objects> stack;

   Object getObject(); // takes from stack, if it's empty creates new one
   Object returnObject(); // adds to stack
}
Ilya Gazman
quelle
3
Die Verwendung von Pool für kleine Objekte ist eine ziemlich schlechte Idee. Zum Booten benötigen Sie einen Pool pro Thread (oder der gemeinsame Zugriff beeinträchtigt die Leistung). Solche Pools sind auch schlechter als ein guter Müllsammler. Zuletzt: Der GC ist ein Glücksfall beim Umgang mit gleichzeitigem Code / Strukturen - viele Algorithmen sind wesentlich einfacher zu implementieren, da natürlich kein ABA-Problem vorliegt. Ref. Das Zählen in einer gleichzeitigen Umgebung erfordert mindestens eine atomare Operation + Speicherzaun (LOCK ADD oder CAS auf x86)
Bests
1
Die Verwaltung der Objekte im Pool kann mehr teurer als der Garbage Collector laufen lassen.
Thorbjørn Ravn Andersen
@ ThorbjørnRavnAndersen Im Allgemeinen stimme ich Ihnen zu, aber beachten Sie, dass das Erkennen eines solchen Unterschieds eine ziemliche Herausforderung darstellt. Wenn Sie zu dem Schluss kommen, dass GC in Ihrem Fall besser funktioniert, muss es ein sehr einzigartiger Fall sein, wenn ein solcher Unterschied von Bedeutung ist. Umgekehrt kann es jedoch sein, dass der Objektpool Ihre App speichert.
Ilya Gazman
1
Ich verstehe dein Argument einfach nicht? Es ist sehr schwer zu erkennen, ob GC schneller ist als Objektpooling? Und deshalb sollten Sie Objektpooling verwenden? Die JVM ist für saubere Codierung und kurzlebige Objekte optimiert. Wenn es um diese Frage geht (was ich hoffe, wenn OP eine Million davon pro Sekunde generiert), sollte dies nur dann der Fall sein, wenn es einen nachweisbaren Vorteil gibt, auf ein komplexeres und fehleranfälligeres Schema wie das von Ihnen vorgeschlagene umzusteigen. Wenn dies zu schwer zu beweisen ist, warum dann?
Thorbjørn Ravn Andersen
0

Objektpools bieten enorme (manchmal 10-fache) Verbesserungen gegenüber der Objektzuweisung auf dem Heap. Aber die obige Implementierung unter Verwendung einer verknüpften Liste ist sowohl naiv als auch falsch! Die verknüpfte Liste erstellt Objekte, um ihre interne Struktur zu verwalten und den Aufwand aufzuheben. 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 Halterobjekt für die Liste aller berechneten Züge eingewickelt werden. Es werden dann nur die Objektreferenzen des Bewegungsinhabers weitergegeben.

Michael Röschter
quelle