Wie behebe ich ein großes und vermutlich bekanntes Memory Leak?

Kokujou

Lieutenant
Registriert
Dez. 2017
Beiträge
948
Ja, Hallo erstmal!

Ich hab ein großes Memory Leak in meinem Programm und ich hab es bereits gefunden. Die Frage ist, wie behebe ich es denn überhaupt?

Mein Setup:
Ich habe ein Spiel. Dieses Spiel erstellt einen Zustandsbaum, der vermutlich das Memory-Leak darstellt. Er rechnet vermutlich 100.000 zustände pro Spielzug aus und verursacht damit natürlich viel Speicher.

Nun wird das Spiel automatisiert - KI gegen KI. Ich lasse 100 Spiele in einem Parallel.For laufen. Und natürlich wird in jedem dieser Spiele in jeder Runde ein neuer Zustandsbaum aufgebaut.

Wie/Wann/Wo ich diesen Zustandsbaum aufbaue wird sich nicht ändern. Und der Speicher-Verbrauch pro Spiel hält sich in Grenzen. Allerdings scheint der Speicher nach jedem Spiel nicht aufgeräumt zu werden.

Meine Theorie: Je nach ANzahl der Kerne laufen X Threads gleichzeitig. Und die brauchen natürlich auch alle diese Resourcen. Also ver x-facht sich theoretisch mein Speicherverbrauch. Auch das ist bekannt. nun hab ich 16GB arbeitsspeicher, die dürften eigentlich nichtmal ansatzweise voll werden.

Zum Vergleich: Eine Runde verbraucht bestenfalls 300-400MB. Folglich dürften 12 Kerne 4GB verbrauchen. Mein Verbrauch liegt bald bei über 10GB Arbeitsspeicehr und das auch nur weil ab da bei meinem Speicher langsam schluss ist und es nicht weiter geht.

Irgendwas scheint da nicht ganz zu funktionieren. Wie funktioniert denn dieser Parallel.For? Startet der etwa einfach mal 100 Threads die sich alle nicht leeren und pseudoparallel laufen?

PS: Es wird um die Programmiersprache C# gehen, tut mir leid dass ich das nicht erwähnt habe.
 
Zuletzt bearbeitet:
Ohne den Code oder die Sprache zu kennen ist es leider schwierig zu erraten woran es liegt.
Ich würde jetzt einfach mal parallel.for mit java (.parallelStream().forEach) in Verbindung setzen, dann müsstest du eigentlich nur nen heap dump erzeugen und dir den ankucken, daran sollte sich erkennen lassen wo es hängt.

// Edit
Ich sehe grad aus einem anderen Thread das es um C# gehen wird.
 
Zuletzt bearbeitet:
Kokujou schrieb:
Startet der etwa einfach mal 100 Threads die sich alle nicht leeren und pseudoparallel laufen?
ja. Was soll er sonst machen? Wenn du das nicht willst, begrenze die Anzahl gleichzeitiger Threads mit
MaxDegreeOfParallelism in einem ParallelOptions Objekt:

C#:
Parallel.ForEach(
    games,
    new ParallelOptions { MaxDegreeOfParallelism = 12 },
    game => { processGame(game); }
);

Je nach Komplexitaet von deiner KI (oder was auch immer daraus geworden ist), solltest du ueber weiteres Sharing von Ressourcen nachdenken. Das kannst aber nur du beurteilen.

Was man hier mal wieder sieht: Paralleliserung von Problemen ist bei weitem nicht so trivial und simpel wie es manche Leute im Forum immer denken, wenn sie sich darueber aufregen dass ihr Spiel oder ihre Anwendung nicht beliebig skaliert ...
 
Zuletzt bearbeitet:
Ah Sorry... ich vergess immer die Sprache zu erwähnen... Ja es ist C# ^^ tut mir leid.

Nun ich dachte er ist so schlau auch nur so viele Tasks zu erzeugen wie es Kerne gibt, dann jeden abgeschlossenen Task anständig zu disposen und all seine Memory-Verbrauchnisse aufzulösen und dann den neuen zu starten... das wäre doch eigentlich das sinnvollste oder?
notfalls kann man das Disposen des alten Threads ja auch machen nachdem schon der nächste eingesprungen ist oder so...

Ich rede übrigens von Parallel.For, da ein zähler für mich sehr wichtig ist.
Ergänzung ()

Ich will euch nicht den ganzen code um die ohren Hauen aber die Parallel-Funktion sieht bei mir so aus:

C#:
Parallel.For<IterationOutput>(0, ExecutionTimes, () => new IterationOutput(), (round, state, output) =>
            {
                var result = PlayNewGame(true, P1Mode, P2Mode);
                if (result == 0)
                    output.Draws++;
                else if (result == 1)
                    output.P1Wins++;
                else
                    output.P2Wins++;
                return output;
            }, finalOutput =>
            {
                lock (synchronizeOutput)
                {
                    totalOutput.P1Wins += finalOutput.P1Wins;
                    totalOutput.P2Wins += finalOutput.P2Wins;
                    totalOutput.Draws += finalOutput.Draws;
                }
            });
Ergänzung ()

Update: Ich hab jetzt mal die Paralleloptions da eingebaut - kein Erfolg.
Ich weiß nicht woran es liegt aber er reduziert niemals Speicher, er scheint einfach nicht aufzuräumen.

ich hab mal manuell den Garbage Collector gestartet mit Maximum und Forced als Optionen, das Ding war zwar extrem langsam danach - verständlich - aber der gleiceh Effekt. Wenn ich im Task manager gucke steigt der Speicherverbrauch immer weiter stetig an ohne auch nur einmal zurück zu gehen.

darum frage ich: Wenn so ein Leak identifiziert ist und das ist er ja größtenteils, wie löse ich ihn? Was mache ich um ordentlich zu disposen?

ich hab z.B. meine States alle in einer List<List<Class>> Datenstruktur gespeichert.
 
Ich fürchte dieser Link wird mir nicht helfen. Darin scheint es größtenteils darum zu gehen Memory Leaks zu finden, aber ich hab sie ja schon gefunden und weiß was den Overhead verursacht.

Außerdem drehen sich die Lösungen um das IDisposable pattern. Gut, aber das kann ich nicht benutzen. Ich schreibe nicht auf Files und benutze auch sonst keine Datenstrukturen die das Interface implementieren. Alles was ich habe steht in einer Liste und die ist nicht disposable.

Ich hab schon gegoogelt aber es gibt keine "zerstöre diese Klasse auf der Stelle"-Funktion

[edit: sorry dass ich erst auf englisch geschrieben hab >.<]
 
Zuletzt bearbeitet:
Ich würde anstatt dem lock() erstmal auf Interlocked.Add(ref totalOutput.P1Wins, finalOutput.P1Wins) umsteigen. Sollte die Effizienz erhöhen.

Das mit dem RAM-Verbrauch ist tatsächlich merkwürdig. Im Normalfall sollte der GC wenn du keine Referenzen mehr auf das/die Objekt/e hast, aufräumen.
 
Zuletzt bearbeitet:
Der RAM Verbrauch multipliziert sich nicht mit der Anzahl der Kerne/Threads. Eine Instanz existiert einmal. Parallel.For ist so schlau und startet standardmäßig so viele threads wie Kerne/"Hardware-"Threads vorhanden sind.

Du sagst du speichert Dinge in einer Liste. Lerrst du diese denn auch wieder, bzw. entfernst Elemente, die du nicht mehr brauchst? Dein Memory Leak kommt sicher nicht von den Threads.
 
Nun, die Liste existiert in einer Klasse die darum gewrappt ist und die ist dann auch wieder eine Eigenschaft einer anderen Klasseninstanz.

Diese Klasseninstanz setze ich dann bei einer neuen Runde auf eine neue instanz und dann müsste die alte ja eigentlich Verweislos herumliegen, oder?

Um es mal auszusprechen:
Ich hab eine Klasse "Spielfeld", darin gibt es eine Klasse "Spieler".
Meine KI ist von der Klasse KI und erbt von dieser Spieler-Klasse.
Diese KI hat nun eine Eigenschaft State-Tree und das ist wiederum ein Wrapper um eine List<List<Spielfeld>>
Und Spielfeld kann man als Ansammlung von Value-Types verstehen.

Soweit so gut. Wenn das Spiel startet generiere ich mir also ein Zufälliges Spielfeld, meine KI errechnet für jeden Spielzug 8! Spielzüge als Datenstruktur Spielfeld und wenn ich ein neues Spielfeld erzeuge ist die alte Instanz weg.

Ja,, ich weiß, die Struktur ist schrecklich und recht verzwickt aber es ist ein älteres Projekt und es ist quasi fast fertig. es jetzt noch umzustrukturieren würde nochmal ein paar Semester brauchen.
 
Du solltest lernen mit dem Debugger umzugehen und dein Programm profilen.

Wenn du keine externen Ressourcen hast, die zu disposen sind und du keine Referenzen mehr hast, dann wird der GC diese aufräumen. Punkt. Du hast also noch irgendwo Referenzen auf irgendwas. Falls es nicht zu viel ist, poste den Code. Ansonsten bleibt dir nur der Debugger. Hier im Dunkeln raten ist nicht zielführend.
 
Genau du hast doch die Ursache für das Memory leak gar nicht gefunden. Du siehst nur dass irgendwo der Speicher nicht freigegeben wird aber nicht wo noch Referenzen bestehen.
Wird der Speicher denn wieder freigegeben wenn du statt 100 nur 2 Instanzen der Klasse erstelltst.
Hast du dich schon mal mit perfview beschäftigt https://blogs.msdn.microsoft.com/vancem/tag/perfview/ oder dem VS memory profiler
beschäftigt
 
nun, ich weiß welche Daten den Memory-Leak verursachen das ist schonmal klar.

Aber ich denke ich brauch mehr Informationen über den GC-Cleaner, vielleicht könnt ihr mich da ein bischen aufklären und ich hoffe euch damit nicht zu sehr zu nerven^^

Ihr habt ja gesehen dass es bei mir sehr verzweigt ist. Da stellt sich mir die Frage gibt es Konstellationen in denen Variablen nicht aufgeräumt werden?

Ich meine Parallelität erzeugt definitiv mehr Speicher, so viel ist schonmal klar. Denn hier laufen Threads gleichzeitig die dann auch gleichzeitig denselben Speicherverbrauch haben. ihr sagt dass der TaskScheduler eigentlich alles brav löscht und nicht einfach alle Thread-Daten hortet bis die ganze Parallel.For Anweisung abgeschlossen ist.

Ganz ehrlich mein programm ist ja recht stabil. Am liebsten würde ich ihm sagen er soll einfach alles wegschmeißen was er zu dem Zeitpunkt im Speicher hat. Aber das geht vermutlich nicht, würde mich zumindest wundern.

Ich könnte meinen Code quasi so umbauen, dass man sich die Ausführung der Spiele als Ausführung eines Prozesses vorstellen kann. Am Ende kann alles was auch nur im entferntesten damit zutun hat ab in die Tonne.

Vielleicht sollte ich es einfach wirklich als extra-Prozess laufen lassen um ganz sicher zu gehen... da ich als rückgabewert eigentlich nur Integer brauche könnte ich es über den ExitCode regeln. Denn bei nem extra Prozess wird am Ende dieses Prozess definitiv aller Speicher der irgendwie mit diesem Prozess zu assoziieren ist aufgeräumt.
 
Kokujou schrieb:
Vielleicht sollte ich es einfach wirklich als extra-Prozess laufen lassen um ganz sicher zu gehen... da ich als rückgabewert eigentlich nur Integer brauche könnte ich es über den ExitCode regeln. Denn bei nem extra Prozess wird am Ende dieses Prozess definitiv aller Speicher der irgendwie mit diesem Prozess zu assoziieren ist aufgeräumt.

Das ist nur Symptom-Bekämpfung. Im Endeffekt lagerst du das Problem einfach nur woanders aus, wo es in dem Moment nicht so schwer wiegt.

Beschäftige dich mit dem MemoryProfiler von VisualStudio. Dort kannst du zu einem Zeitpunkt den Speicherzustand sichern (Snapshot). Nach der Berechnung machst du das Gleiche. Dann kannst du die beiden Zustände vergleichen (z. B. das Delta anzeigen lassen). Dort hab ich bisher immer recht einfach herausgefunden welche Objekte überleben und warum diese überleben (z. B. Event-Referenz). https://docs.microsoft.com/de-de/visualstudio/profiling/memory-usage?view=vs-2019
 
Das Problem ist dass die meisten Profiling Tools bei mir nicht funktionieren. Denn ich arbeite mit Unity3D... wenn das die einzige Möglichkeit ist das zu untersuchen muss ich mal gucken ob da irgendwas geht.

Ich krieg z.B. kein Task-Debugging und auch kein Memory und CPU Profiling hin.

Und immer wenn ich beim Debugging auf Pause gehe, also im laufenden Betrieb an den aktuellen Stand gehe, gibt er mir nur ein Fenster von Wegen nicht verwalteter Code oder so. Alles sehr unschön.
 
In solchen Fällen könntest du dir den Call-Stack anschauen und den solange hochwandern bis du bei deinem Code landest. Du musst natürlich jeden Thread untersuchen, da wahrscheinlich Unity auch Threads erstellt welche z. B. in dem von dir genannten unverwalteten (unmanaged) Code arbeiten.

Pauschal würde ich behaupten, dass es mit Unity 3D bestimmt Möglichkeiten gibt den ausgeführten C#-Code zu debuggen und dementsprechend auch das Laufzeitverhalten zu analysieren.
 
Ja... da muss ich mich halt umhören. Ich hatte gehofft dass es z.B.. bekannte robleme und workarounds für die C# Parallelität Funktionen gibt die da passen ^^

Naja nach längerer Beobachtung scheint es nach der Beschränkung gleichzeitiger Ausführungen immerhin tatsächlich etwas besser geworden zu sein. Es steigt nach wie vor stark an, hat aber eine niedrigere Grenze.
 
Es gibt halt keine bekannten Probleme beim Multi-Threading in C#. Das Problem liegt einzig und allein in deinem Code und nicht bei den Threads oder dem Garbage Collector.

Poste halt mal deinen Code.
 
  • Gefällt mir
Reaktionen: new Account()
sagtmal... wie kann so ein Problem überhaupt auftreten? Ich meine ich bin unerfahren aber auch nicht mehr ganz so blöd. Ich deklariere keine statischen Variablen oder Listen die dann über zustände erhalten bleiben.

Im Grunde ist es ja eigentlich egal was mit einer Datenstruktur passiert. Solange die Instanz = null gesetzt wird müsste sie aufgeräumt werden, das hab ich doch richtig verstanden oder?

Also am Anfang der funktion erstelle ich eine neue Instanz mit einer neuen KI. Nur diese Instanz verfügt über die im Spiel generierten Informationen und Zustandsbäume. Und auch der Zustandsbaum wird immer wieder neu Aufgebaut und sodass der alte dabei eigentlich gelöscht werden müsste.

Also egal wie man es sieht die Datenstruktur verliert jeden Verweis sobald die Funktion abgeschlossen ist. Da es kein IDisposable ist kann ich aus ihr keine using Direktive machen. Aber inzwischen hbae ich sogar explizit an die beiden returns geschrieben, dass er die Instanz auf =null setzen soll.

Also kann diese Instanz nach abschluss eines Threads nicht mehr existieren oder?

wie entstehen Memory Leaks bzw wie sieht ein Beispielhaftes Memory Leak aus? Es scheint ja ein häufigeres Problem zu sein und ich glaube nicht dass heutige Entwickler so blöd sind einfach globale variablen zu deklarieren und sich dann zu wundern warum sie nicht gelöscht werden.
 
wenn du auf meine Bemerkung mit dem using anspielst dann meine ich damit Blöcke in denen IDisposable Variablen definiert werden. Am Ende des BLocks wird automatisch die Dispose Funktion aufgerufen.

Das kann man halt machen wenn man z.B. zum Dateien in Ordnern anlegt und den Ordner am ende des Tests löschen will.

sowas wäre super aber Ich weiß nicht wie ich eine Klasseninstanz manuell lösche und ihren benutzten Arbeitsspeicher freigebe.
Oder ob das überhaupt helfen würde...
Ergänzung ()

was passiert eigentlich wenn in einem Thread weitere Tasks erzeugt werden, könnte das ein Problem werden?

ich benutze nämlich auch Multithreading um meine Zustandsbäume zu erzeugen. Das ist zwar in dem fall doppelt gemoppelt aber das automatisierte gegeneinanderspielen ist ja kein Regelfall.
 
Zuletzt bearbeitet:

Ähnliche Themen

L
Antworten
6
Aufrufe
1.572
L
Zurück
Oben