Jump to content
Unity Insider Forum

GC Performance verbessern.


Helishcoffe

Recommended Posts

So ich bins nochmal.

Bin gerade dabei meinen Terraingenerator zu verbessern. Das was mir am meisten um die Ohren haut, ist momentan der Garbage-Collector. Dieser verursacht in bestimmten Abständen immer ätzende Ruckler - was auch verständlich ist, da beim Generieren des Terrains eine Menge RAM belegt wird, der nach dem fertigen Generieren einiger Chunks wieder freigegeben werden will. 

Sprich: ich habe eine Menge lokaler Variablen die belegt werden und dann wieder vom GC freigegeben werden. Doch nun meine Frage: habt ihr ein paar Tricks und tipps für mich, wie ich diesen Framedrop vom GC umgehen kann?

Eine Idee von mir war: Da ich einige Vector3 benutze und diese ja Structs sind, hatte ich mir gedacht eine eigene Vector3 als Klasse zu implementieren. Diese würde ich dann in einem Array speichern in einer globalen Variable. Somit dürften die Vector3's erstmal nicht freigegeben werden und somit würde der GC auch erstmal nicht greifen, oder? Um den RAM dann natürlich nicht vollzumüllen könnte ich dieses Array wieder dereferenzieren wenn z.B. der Chunk kontrolliert despawnt wird. Das despawnen von Chunks soll dann deutlich langsamer geschehen als das Generieren von Chunks. Somit hätte der GC genügend zeit die dereferenzierten Vector3's "einzusammeln". Ist das ein plausibler Ansatz oder verstehe ich hier etwas völlig falsch. Muss dazu sagen, dass ich mich sonst nie mit Garbage-Collection auseinandersetzen musste und mir deshalb auch jegliche Erfahrung fehlt. 

Wäre um jede Hilfe dankbar.

 

EDIT: ich habe das ganze mal ganz grob mit GC.GetTotalMemory() getestet. Ergebnis: Pro Chunk werden ca. 180.000 bis 250.000 Bytes belegt. Das ist natürlich eine Menge, aber ich weiß wirklich nicht wie ich diese Anzahl an Bytes großartig verringern kann. 

Link zu diesem Kommentar
Auf anderen Seiten teilen

Das könntest du machen, ja.
Dass deine Daten auf dem Stack landen ist bei 0,25mb / chunk sowieso schonmal ausgeschlossen, also lungern die dir sowieso irgendwo auf'm Heap rum.

Allgemein zur Speichermenge:
Das kommt drauf an wie deine Chunks so aussehen. Bei Voxel Strukturen z.B. greift man oft gerne zu Octree's um Speicher zu sparen. Das ist dann bloß sowas wie ein LOD System. Was du gerade nicht brauchst, braucht auch nicht im Speicher sein.

Link zu diesem Kommentar
Auf anderen Seiten teilen

vor 28 Minuten schrieb Life Is Good:

Das könntest du machen, ja.
Dass deine Daten auf dem Stack landen ist bei 0,25mb / chunk sowieso schonmal ausgeschlossen, also lungern die dir sowieso irgendwo auf'm Heap rum.

Okay dann werde ich das einfach mal ausprobieren. Kann ja nicht viel bei schief gehen

 

vor 28 Minuten schrieb Life Is Good:

Allgemein zur Speichermenge:
Das kommt drauf an wie deine Chunks so aussehen. Bei Voxel Strukturen z.B. greift man oft gerne zu Octree's um Speicher zu sparen. Das ist dann bloß sowas wie ein LOD System. Was du gerade nicht brauchst, braucht auch nicht im Speicher sein.

Jaa ich denke mal das wäre etwas, was ich mir in der späteren Entwicklungsphase meines Spiels mal anschauen kann. Hauptsache ich kriege den GC erstmal unter Kontrolle.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Du solltest Speicher nur 1x allokieren und diesen nicht wieder freigeben. Dadurch tritt der GC gar nicht erst in Aktion. Es macht keinen Sinn Speicher zu belegen, diesen wieder frei zu geben und den Speicher gleich wieder neu zu belegen, was du bei deinem Terraingenerator vermutlich machst. Man macht so etwas normalerweise über einen Pool. Vielleicht kennst du das Prinzip ja bereits von einem GameObjekt-Pool. Man kann dies aber auch für verwendete Speicherstrukturen machen. Was du dabei für eine Speicherstruktur verwendest ist dir überlassen, wichtig ist eben nur nicht ständig neuen Speicher zu allokieren, sondern bereits verwendete Strukturen im Speicher wiederzuverwenden. Du kannst deinen Speicher gebündelt freigeben, wenn du eine neue Szene lädst oder eben der Terraingenerator nicht mehr benötigt wird. Ich spreche hier immer von "freigeben", damit meine ich in C#, daß du Referenzen auf einer Speicherstruktur entweder hältst oder verwirfst.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Am 15.12.2017 um 22:46 schrieb Helishcoffe:

Eine Idee von mir war: Da ich einige Vector3 benutze und diese ja Structs sind, hatte ich mir gedacht eine eigene Vector3 als Klasse zu implementieren. Diese würde ich dann in einem Array speichern in einer globalen Variable. Somit dürften die Vector3's erstmal nicht freigegeben werden und somit würde der GC auch erstmal nicht greifen, oder?

Structs sind generell nicht vom GC betroffen, sobald du aber eine eigene Klasse machst, dann hast du den GC am Hals. 

Ich bin schon seit einiger Zeit dabei einen Octree/Quadtree so gut es geht GC-frei umzusetzen, dabei sind alle Nodes structs. Diese speichere ich sie in einem dictionary mit einem Index. Durch einfache Mathematik kann ich dann aus einer Position den Index ermitteln und auch den ganzen Node-tree und kann anhand der Indices nachschauen, ob ein Node existiert. Da ist momentan das Dictionary der Hacken, hier experimentiere ich derzeit mit eigenen Listtypen. Mit unsafe code und dem c# 7 ref return könnte man sehr effiziente Liste erzeugen.

Link zu diesem Kommentar
Auf anderen Seiten teilen

vor 15 Stunden schrieb Zer0Cool:

Du solltest Speicher nur 1x allokieren und diesen nicht wieder freigeben. Dadurch tritt der GC gar nicht erst in Aktion. Es macht keinen Sinn Speicher zu belegen, diesen wieder frei zu geben und den Speicher gleich wieder neu zu belegen, was du bei deinem Terraingenerator vermutlich machst

Ja das stimmt, allerdings bin ich ja dazu gezwungen diesen Speicher irgendwann wieder freizugeben, der Terrain ja "unendlich" groß werden kann. Somit muss ich Chunks die nicht mehr in der Nähe des Spielers sind freigeben damit der RAM ja nicht randvoll wird. 

vor 15 Stunden schrieb Zer0Cool:

Man macht so etwas normalerweise über einen Pool. Vielleicht kennst du das Prinzip ja bereits von einem GameObjekt-Pool. Man kann dies aber auch für verwendete Speicherstrukturen machen

Also wenn ich das dann richtig verstanden habe, würde ich also den Speicher gar nicht freigeben, sondern beim Generieren eines neuen Chunks, einfach den Speicher eines alten nicht mehr benötigten Chunks verwenden? um es mal grob auszudrücken.

vor 1 Stunde schrieb runner78:

Structs sind generell nicht vom GC betroffen, sobald du aber eine eigene Klasse machst, dann hast du den GC am Hals. 

Okay danke das wusste ich noch gar nicht :) 

 

Ich werde mir Eure Ideen mal anschauen :) 

Link zu diesem Kommentar
Auf anderen Seiten teilen

Die Aussage ist auch Blödsinn.
Sobald dein Struct auf dem Heap laden soll, fällt das selbstverständlich unter den Aufgabenbereich des GC.

Und wenn wir es ganz genau nehmen, stimmt das hier auch nicht

Zitat

Du solltest Speicher nur 1x allokieren und diesen nicht wieder freigeben. Dadurch tritt der GC gar nicht erst in Aktion.

Der Garbage Collector ist nämlich nicht bloß für's Überwachen & Freigeben von Speicher, sondern auch für's Allokieren zuständig.

Allgemein als Tipp noch dazu:
Wenn du dich nicht im unsicheren Code wohlfühlst (Ich weiß ja nicht wie es da um deine Kenntnisse steht), solltest du damit auch nicht in Unity rumhantieren. Damit richtet jemand der sich nicht auskennt höchstwahrscheinlich mehr Schaden als Gut an.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Zitat

Also wenn ich das dann richtig verstanden habe, würde ich also den Speicher gar nicht freigeben, sondern beim Generieren eines neuen Chunks, einfach den Speicher eines alten nicht mehr benötigten Chunks verwenden? um es mal grob auszudrücken.

Genau. Ein verwendeter Speicherbereich wird nicht freigegeben und statt eines neuen Chunks zu allokieren wird ein alter nicht mehr verwendeter Chunk wieder verwendet. Dadurch das du die Referenz auf den den alten Chunk nicht freigibst kommt der GC nicht zum Zuge und dadurch daß der Speicher des alten Chunks wiederverwendet wird, sparst du zusätzlich Performance. Wenn ein neuer Chunk gebraucht wird wird er erzeugt, ansonsten werden alte Chunks wiederverwendet (Pool-Prinzip).

Link zu diesem Kommentar
Auf anderen Seiten teilen

vor 8 Stunden schrieb Zer0Cool:

Genau. Ein verwendeter Speicherbereich wird nicht freigegeben und statt eines neuen Chunks zu allokieren wird ein alter nicht mehr verwendeter Chunk wieder verwendet. Dadurch das du die Referenz auf den den alten Chunk nicht freigibst kommt der GC nicht zum Zuge und dadurch daß der Speicher des alten Chunks wiederverwendet wird, sparst du zusätzlich Performance. Wenn ein neuer Chunk gebraucht wird wird er erzeugt, ansonsten werden alte Chunks wiederverwendet (Pool-Prinzip).

Sehr gut das hat mir schonmal sehr geholfen.

Eine Frage habe ich noch: Wenn ich dann z.B. ein Objekt chunk1 habe und diesem Objekt nun ein Chunk zugewiesen wurde, der nicht mehr gebraucht wird. Kann ich diesem Objekt dann einfach eine neue Instanz direkt zuweisen ohne dass der GC greift? Oder muss ich die Daten in dem Objekt Manipulieren damit die alte Instanz sozusagen nicht überschrieben wird sondern weiter genutzt wird? 

Link zu diesem Kommentar
Auf anderen Seiten teilen

Zitat

Eine Frage habe ich noch: Wenn ich dann z.B. ein Objekt chunk1 habe und diesem Objekt nun ein Chunk zugewiesen wurde, der nicht mehr gebraucht wird. Kann ich diesem Objekt dann einfach eine neue Instanz direkt zuweisen ohne dass der GC greift? Oder muss ich die Daten in dem Objekt Manipulieren damit die alte Instanz sozusagen nicht überschrieben wird sondern weiter genutzt wird? 

Soweit ich weiß, muss die Referenz auf den alten Chunk erhalten bleiben, das bedeutet du hast quasi aktive Chunks und inaktive Chunks, musst aber alle Referenzen auf diese Chunks halten.  Ein Beispielimplementation wäre eine Klasse "Chunk" und du erzeugst neue Instanzen dieser Klasse 1x zu Programmstart und 1x auf Abruf. Zu Programmstart wird eine gewisse Anzahl neuer Instanzen erzeugt und in eine Liste ("ActiveChunks") eingefügt. Zusätzlich existiert eine Liste "InactiveChunks". Das bedeutet es gibt eine Liste mit aktiven Chunks ("ActiveChunks") und eine weitere Liste wo du die inaktiven Chunks ("InactiveChunks") einsortierst. Wird ein neuer Chunk gebraucht, dann wird er aus der Liste "InactiveChunks" entfernt und in die Liste ""ActiveChunks" eingefügt. Ist die Liste "InactiveChunks" leer wird ein neuer Chunk (new Chunk) erzeugt und dort eingefügt. Auf diese Weise bleiben über die Listen die Referenzen erhalten und der GC darf sie nicht abräumen. Normalerweise erzeugt man dabei zu Programmstart gleich eine gewisse Menge an aktiven Chunks (ca. so viele wie dein Terraingenerator brauchen wird). Die ideale Menge kannst du im Betrieb auslesen. Sollte die Kapazität später nicht ausreichen macht dies nichts, da du ja auch später noch neue Chunks nachlegen kannst.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Soo ich habe jetzt einen ObjektPool integriert. Ich bin erstaunt wie viel Performance man so raushohlen kann. Habe es genau so gemacht wie @Zer0Cool es vorgeschlagen hat. Meine InactiveChunks Liste pegelt immer bei etwa 300 bis 500 Chunks. Sind es mal mehr als 500 Chunks, so werden diese nach und nach aus dem Speicher geworfen, bis es wieder 500 sind. So kann ich verhindern, dass der Speicher vollgemüllt wird, da in manchen Szenarien sehr viele Chunks aufeinmal generiert werden und diese dann alle in der InactiveChunks-Liste rumlungern. Habe allerdings zurzeit noch kleine Bugs die ich beheben muss. Das Konzept aber funktioniert wunderbar! 

Danke für eure Hilfe :) 

Link zu diesem Kommentar
Auf anderen Seiten teilen

Archiviert

Dieses Thema ist jetzt archiviert und für weitere Antworten gesperrt.

×
×
  • Neu erstellen...