Performance einer Monte-Carlo-Simulation verbessern

haaron

Mitglied
Hallo!

Ich verwende ein selbst geschriebenes Java-Programm um eine Monte-Carlo-Simulation durchzuführen. Ohne jetzt ins Detail gehen zu wollen, geht es dabei um Folgendes: Es wird die Entwicklung eines System konstanter Größe N (Größenordnung einige tausend bis einstelliger Millionenbereich) simuliert. Es werden pro Simulation 100.000 Simulationsschritte durchgeführt, wobei sich ein Simulationsschritt auf ein durchgeführtes Ereignis pro Objekt im System (also N Ereignisse) bezieht. Vor jedem Ereignis wird mit konstanten Wahrscheinlichkeiten ausgewählt, welche Art von Ereignis durchgeführt werden soll, denn es existieren prinzipiell verschiedene Ereignisse. Diese haben aber alle gemeinsam, dass sie eines oder zwei Objekte des Systems auswählen und anhand des Aktuellen Zustands des Systems die Eigenschaften des ausgewählten Objekts verändern. Ich habe diese Simulation in mehreren Versionen implementiert und dabei festgestellt, dass die Simulation gleicher Systeme in den unterschiedlichen Implementierungen jeweils unterschiedlich lange braucht. Folgende Versionen habe ich implementiert:

- Eine "Basic-Version", die nicht flexibel angepasst werden kann, also nur genau eine Art von System simulieren kann, dadurch konnte ich mir jegliche Vererbungen etc. sparen. Ich habe hier keinerlei Getter/Setter verwendet, sondern einfach alle Felder als public deklariert.
- Eine Version, welche zwar nicht wesentlich flexibler ist, aber die Style(?)-Vorgaben befolgte, also beispielsweise Getter/Setter
- Eine flexible Version, hier wurde viel mit Vererbung gearbeitet, es sind neue Arten von Systemen definierbar, sowie neue Ereignisse, etc.

Nun ist diese flexible Version leider am unperformantesten. Prinzipiell sind die Unterschiede hier nicht extrem, aber der benötigte Zeitaufwand wächst mit Systemgröße linear und bei den "großen" Systemen mit einer Größe macht dann ein Faktor 4-5 schon einen Unterschied, wenn statt 24h eben 5 Tage benötigt werden. Nun kann mit den Angaben die ich hier bisher gemacht habe vermutlich noch keine Diagnose gestellt werden, deshalb wollte ich nachfragen, ob ihr allgemeine Tipps habt, auf was ich bei einer solchen Art von Simulation achten sollte. Wenn ihr gute Beiträge, Webseiten, etc. dazu kennt nehme ich das alles gerne! Ich bin aber euch mehr Anfänger, deshalb fehlt mir eben auch der Blick "hinter die Kulissen".

Damit vielleicht noch spezifischere Tipps gegeben werden können hier nochmal ein paar weitere Infos, ich erkläre es mal recht abstrakt, weil es sonst vielleicht noch komplizierter wird:

Jedes Ereignis kann angenommen oder abgelehnt werden. Dafür wird das Ereignis durchgeführt (Details dazu gleich) und anschließend eine Wahrscheinlichkeit berechnet dieses Ereignis anzunehmen oder abzulehnen, die entsprechende Wahrscheinlichkeit wird mit java.lang.Math.exp berechnet, das Argument der Funktion wird berechnet aus dem Zustand des Systems vor der Durchführung des Ereignissen und dem Zustand danach. Wird das Ereignis abgelehnt, dann wird das bereits durchgeführte Ereignis einfach wieder rückgängig gemacht (alle geänderten Variablen auf ihren Ausgangszustand gesetzt).

Nun zu den Ereignissen: Für jedes Ereignis existiert eine eigene Klasse, die von der Klasse AbstraktesEreignis erbt. In der abstrakten Klasse wird nur die Methode zur Berechnung der Wahrscheinlichkeit implementiert. Jedes Ereignis besitzt eine Methode perform() zur Durchführung. Es existiert zusätzlich eine Klasse, die sich um die Verwaltung der Ereignisse kümmert: Hier werden jeweils Objekte der Ereignisse gespeichert und hier erfolgt die Auswahl eines Ereignisses anhand der Ziehung der Zufallszahlen. Die Ereignisse verändern jeweils die Objekte des Systems, dafür besitzen sie entweder eine Referenz zum Array der Objekte (welcher ursprünglich der System-Klasse ist) oder eine eigene Version davon, in dem beispielsweise die Ereignisse nach einer bestimmten Eigenschaft der Objekte sortiert vorliegen.

Nun ist der Vorgang einfach folgender, dass jedes Mal im EreignisManager ein zufälliges Ereignis ausgewählt wird, es wird dessen perform()-Methode aufgerufen und in der Perform-Methode wird dann das Ereignis angenommen oder abgelehnt. In den Perform-Methoden werden dann beispielsweise auch noch weitere Zufallszahlen gezogen, es werden ArrayLists erstellt, Schleifen durchlaufen, if-Bedingungen geprüft, etc.

Und dazu noch eine etwas konkretere Frage: Ich verwende Beispielsweise ArrayLists, weil ich Objekte "sammeln" muss, die eine bestimmte Eigenschaft besitzen. Da mache ich mithilfe einer for-Schleife, in der mithilfe einer if-Bedingung einfach gefragt wird, ob die Eigenschaft erfüllt wird. Nun ist es eben so dass dadurch die Länge der ArrayList variieren kann, weshalb ich auch keinen Array verwende. Allerdings kann die Länge höchstens einen (konstanten) Maximalwert annehemen. Ist es dann beispielsweise performanter, einen Array zu erstellen, der Länge dieses Maximalwerts und dann später beim iterieren über diesen jeweils zu überprüfen ob das jeweilige Objekt null ist und dann die Schleife zu beenden?



Ich hoffe das alles gut verständlich ist und ich die wichtigsten Infos genannt habe. Ich würde mich sehr sowohl über allgemeine, als auch spezifische Tipps freuen und kann gerne weitere Infos geben, falls benötigt.

Viele Grüße und schonmal herzlichen Dank!
 
- Eine "Basic-Version" [...] Ich habe hier keinerlei Getter/Setter verwendet, sondern einfach alle Felder als public deklariert.
Es macht von der Performance Null/0 Unterschied, ob ein Getter/Setter verwendet wird, oder das Feld direkt angesprochen wird: Jede JVM seit ca. 1998 inlined Getter/Setter-Aufrufe agressiv.
Ist es dann beispielsweise performanter, einen Array zu erstellen, der Länge dieses Maximalwerts [...]
Ja. Eine ArrayList wird mit einer initialen Kapazität von 10 Einträgen erzeugt. Die ArrayList selbst hat also eine Kapazität (maximale Anzahl an Einträgen, bis das interne Array vergrößert werden muss) und eine Länge (die effektive Anzahl gerade hinzugefügter Elemente).
Wenn die Länge die aktuelle Kapazität übersteigt, muss ein neues Array alloziiert werden (welches aktuell 1.5x so groß ist wie das alte) und kopiert werden.
Das kann man sich sparen, wenn du den ArrayList(int) Konstruktor verwendest und eine maximale Kapazität vorinitialisierst.
Ich hoffe das alles gut verständlich ist und ich die wichtigsten Infos genannt habe.
Nein. Bei Performance in Größenordnungen von Millionen von Aufrufen geht es um ganz andere Dinge. Z.B. Speicher-/Cachelokalität. Wie genau sieht dein Speicherzugriffsmuster aus? Greifst du hier und dort wahlweise/random auf Array/List-Element zu (Cache-Thrashing)? Wie häufig alloziierst du wann welche Objekte? Wie ist überhaupt die Komplexitätsklasse des gesamten Algorithmus und kann man das eventuell verbessern?
Hier musst du einfach etwas Code zeigen. Den kann man dann Stück für Stück hinsichtlich Performance untersuchen und eventuell Verbesserungsvorschläge machen.

Was du jetzt aber schon machen kannst: Nutze einen Sampling-basierten Profiler (z.B. https://github.com/jvm-profiling-tools/async-profiler ), um erst einmal herauszufinden bzw. zu messen, welche Codestellen wie viel an Zeit kosten.
Denn Verbesserung kann man nur erreichen, wenn man weiß, wo man verbessern muss bzw. wo der Flaschenhals eigentlich ist.
 
Ohne den problematischen Code zu kennen, lässt sich nicht viel dazu sagen (ohne Glaskugel zumindest).

deshalb wollte ich nachfragen, ob ihr allgemeine Tipps habt, auf was ich bei einer solchen Art von Simulation achten sollte. Wenn ihr gute Beiträge, Webseiten, etc. dazu kennt nehme ich das alles gerne! Ich bin aber euch mehr Anfänger, deshalb fehlt mir eben auch der Blick "hinter die Kulissen".

 
Es macht von der Performance Null/0 Unterschied, ob ein Getter/Setter verwendet wird, oder das Feld direkt angesprochen wird: Jede JVM seit ca. 1998 inlined Getter/Setter-Aufrufe agressiv.
Unnötige Objekterzeugungen vermeiden ist sehr wichtig. Er hat ja auch schon angegeben, dass die zweite und dritte Variante viel langsamer ist als die erste. Die Tatsache lässt sich auch schlecht wegdiskutieren. Hast du schonmal etwas mit BigData gemacht?
 
Unnötige Objekterzeugungen vermeiden ist sehr wichtig. Er hat ja auch schon angegeben, dass die zweite und dritte Variante viel langsamer ist als die erste. Die Tatsache lässt sich auch schlecht wegdiskutieren. Hast du schonmal etwas mit BigData gemacht?
Was hat mein Quote in deinem Post - in dem es darum ging, dass Getter/Setter inlined werden - denn damit zu tun, dass man nicht unnötig viele Objekte erzeugen soll?
Natürlich ist das Erzeugen von sehr vielen Objekten schlecht. Ich habe nirgendwo etwas anderes behauptet.
 
Vielleicht wäre es ein Ansatz, weniger in Objekten, als in Datensätzen zu denken und auf eine entsprechende Datenbank zu setzen.

Mir geht da gerade ein ECS (Entity Component System) durch den Kopf, wie man es in der Spieleprogrammierung verwendet.
 
Ebenfalls solltest du mal einen Lauf mit VisualVM oder aehnlichem beobachten, um festzustellen ob du unnoetig Klassen erzeugst und verwirfst, und somit viel Speicherdruck hast. Also dass der Garbage Collector haeufig und lange laufen muss oder ob eben nicht. Um so oefter der Garbage Collector laufen muss, um so langsamer wird alles. Eventuell kannst du auch mit dem erhoehen der maximalen JVM Speichergroesze (zum Beispiel -Xmx 4g) dazu beitragen dass das Programm schneller laeuft. Wenn du zum Beispiel nur 2 Gigabyte zuweist, und die Simulation in etwa 1,6 Gigabyte um den Zustand zu halten, dann wird der Garbage Collector sehr of laufen, weil die JVM versucht so viel Speicher wie moeglich freizuhalten.
 

Neue Themen


Zurück
Oben