Laden...

Deserialisieren: RAM wird voll

Erstellt von digi333 vor 9 Jahren Letzter Beitrag vor 9 Jahren 4.590 Views
D
digi333 Themenstarter:in
290 Beiträge seit 2006
vor 9 Jahren
Deserialisieren: RAM wird voll

Ich hab ein Problem mit einer Methode und befürchte, dass der Speicher nicht frei geräumt wird, da der Arbeitsspeicher richtig voll wird mit jedem Durchlauf der Variablen der FOR-Schleife. Ideen?


...
for(...)
{
   string[] blocks;
   Deserialisiere(out blocks, prefix + "_classes.string[]");
   ...
   blocks = null;
   GC.Collect();

}
...


public static void Deserialisiere<T>(out T objekt, string path)
        {
            BinaryFormatter binF = new BinaryFormatter();
            FileStream fs = new FileStream(path, FileMode.Open);
            objekt = (T)binF.Deserialize(fs);
            fs.Close();
            fs.Dispose();
        }

G
497 Beiträge seit 2006
vor 9 Jahren

versuch doch mal, dein Programm solang laufen zu lassen, bis kein Speicher mehr übrig ist. Wenn du das schaffst, kannst du dich über nicht funktionierende Garbage Collection beschweren. Aber der GC räumt nicht immer gleich dann auf, wenn man es gerne hätte sondern oft erst dann, wenn er muss. Und von Hand aufrufen muss man ihn normalerweise auch nicht.

D
digi333 Themenstarter:in
290 Beiträge seit 2006
vor 9 Jahren

"GC.Collect();" hab ich eingebaut, da ich hoffte, dass ich das Speicherproblem beseitigen kann. Das Programm beendet nicht, da die FOR-Schleife irgendwann eine Out-Of-Memory Exception verursacht. Wenn ich mir den Taskmanager anschaue, wächst die Speicherbelegung kontinuierlich mit der Größe der deserialisierten Datei. Bedeutet: Deserialisiere Datei (40 MB) -> Arbeitsspeicher steigt an auf ca. 40 MB (Okay) -> mache irgendwas mit "blocks" -> Gehe zur nächsten Datei (auch wieder 40 MB)

Wenn die Garbage Collection richtig funktionieren würde, würde sie durch schließen der Klammer der FOR-Schleife verstehen, dass die ersten 40 MB der ersten Datei nicht mehr benötigt wird und "blocks" im Arbeitsspeicher wieder frei gibt. Das sieht aber nicht so aus. Es wächst kontinuierlich um 40 MB bis zur Exception.

16.806 Beiträge seit 2008
vor 9 Jahren

Zu behaupten, dass der GC nicht richtig funktionieren würde, deutet darauf hin, dass Du nicht verstehst, wie er funktioniert bzw. das Framework an für sich.

Ja, der GC räumt nicht sofort ab. Auch das Abräumen kostet Zeit und erfolgt normalerweise im "Idle" oder eben wenn er muss.
Der Task-Manager hat überhaupt keine Aussagekraft, da .NET für Prozesse nicht nur RAM verwendet, sondern zeitgleich beim Ansteigen auch größere Blöcke reserviert. Es kann also sein, dass die Anwendung erst 50 MB reserviert, und nur 30MB wirklich braucht. Braucht sie 45 MB reserviert sie direkt mal Beispielsweise 100MB. 50MB werden erst dann wieder freigegeben, wenn eine gewisse Zeit lang nur 30MB gebraucht wird.
Das ist das (beispielhafte) Verhalten. ⚠ RAM ist nicht dazu da, dass er leer ist.

Daher ist Deine Aussage völlig irrelevant.
Ob in Deiner Anwendung ein Leck existiert ist mit dem Quellcode nicht zu sehen.
So wie Du Close/Dispose verwendest sieht das jedenfalls auch nicht so aus, dass Du weißt, was Close und Dispose tut - und wie es "optimal" wäre (Stichwort using()).

Dass der RAM um 40MB steigt, wenn eine 40MB Datei eingelesen wird: das glaube ich nicht.
Schließlich benötigt das Framework nicht nur 40MB speicher, sondern auch zusätzliche Informationen, die ebenfalls Speicher kosten. Nicht selten, wird doppelt so viel RAM bei der Serialisierung benötigt.
Wenn Du ein Leck vermutest nimm einen Profiler zur Hand (ab 2013 in VS eingebaut); damit siehst Du Referenzen/offene Handles/vergessene Ressourcen, die das Wegräumen verhindern. Mit dem Taskmanager ist es wie mit einer Glaskugel...

PS: OutOfMemory hat nicht zwangsläufig was mit dem Speicher zutun.
Viel eher sagt die Fehlermeldung aus, dass eine Ressource nicht mehr zur Verfügung steht. Das kann durchaus der RAM sein, aber auch zB. Handles, die nicht korrekt freigegeben werden.

D
digi333 Themenstarter:in
290 Beiträge seit 2006
vor 9 Jahren

Vielen Dank für die "Belehrung". Ich erwarte nicht, dass die GC sofort aufräumt. Es ist jedoch seltsam, wenn ein Programm rasend ansteigt (12 GB Ram Unterschied) und es einfach in die Exception rennt. Man möchte dann doch versuchen das Programm zu retten und die nicht benötigte Speicherbelegung wieder zurück zu erhalten was der Grund für das "Dispose" und das "GC.Collect".

Mit dem Profiler kenne ich mich noch nicht aus, aber ich werde es mir genauer anschauen.

Meine Grundidee war es Code Fragmente zu zeigen, Problembeseitigungen zu zeigen und zu fragen ob ich ein Grundproblem in "Deserialisieren" hab.

49.485 Beiträge seit 2005
vor 9 Jahren

Hallo digi333,

nach deinem Startbeitrag musste man wirklich davon ausgehen, dass du dir unnötige Sorgen machst. Erst in deiner ersten Antwort schreibst du was von einer Exception und selbst in deiner zweiten Antwort schreibst du nichts genaues darüber, obwohl es doch die Exception ist, die überhaupt erst zeigt, dass es hier ein Problem gibt und wo dessen Ursachen liegen könnten. Schau mal bitte in [Hinweis] Wie poste ich richtig? Punkt 5 und liefere die dort aufgeführten Informationen. Vorher ist alles nur Spekulation.

herbivore

D
digi333 Themenstarter:in
290 Beiträge seit 2006
vor 9 Jahren

Entschuldigt bitte die fehlende Information, dass das Programm in eine Exception läuft.

Ich konkretisiere meine Frage. Kann es sein, dass das Programm mit diesem Code-Fragment innerhalb der FOR-Schleife mit Hilfe der Funktion Deserialisiere() immer wieder ein neues Objekt erzeugt welches beim Durchlauf nicht wieder abgeräumt wird? Würdet ihr die Funktion genauso schreiben?

16.806 Beiträge seit 2008
vor 9 Jahren

Insgesamt sieht das für mich so aus, als ob Du blocks noch irgendwo verwendest.
Sei Dir über den Typ String im Klaren: es ist kein "simples Objekt". Mit jeder (normalen, aus Parameter-Sicht) Übergabe wird der String dupliziert.

Ein erneutes Erstellen hat also durchaus die doppelte Menge an Speicher zufolge, sofern Du in der Zwischenzeit den String irgendwo übergeben hast. Das ist aus dem Code nicht ersichtlich.
Aber auf rumraten hab ich ehrlich gesagt auch wenig Lust 😃

Und nein, so würde ich den Code nicht schreiben.
Ich versuche out wie es nur möglich ist zu vermeiden und ein Stream gehört immer in ein using().

49.485 Beiträge seit 2005
vor 9 Jahren

Hallo digi333,

die Deserialisiere-Methode sieht ok aus und die For-Schleife sieht ok aus, bis auf ist wie gesagt im Grunde unnötige CG.Collect. Statt eines direkten Aufrufs von Dispose kann man besser die using-Anweisung verwenden.

Wenn du trotzdem eine Exception bekommst, wird das andere Gründe haben. Welche ist ohne nähere Information zu Exception kaum zu sagen.

Hallo Abt,

blocks ist ein String-Array, also ein Referenztyp. Bei der Parameterübergabe wird da nichts dupliziert (außer der Referenz). Selbst wenn blocks ein String wäre, wäre das auch ein Referenztyp, und entsprechend würde auch da nicht dupliziert.

Richtig ist aber, dass das Objekt, auf das blocks referenziert, nicht freigegeben werden kann, solange durch Parameterübergabe oder sonstige (Zuweisungs-)Operationen noch mindestens eine weitere Referenz darauf existiert.

herbivore

16.806 Beiträge seit 2008
vor 9 Jahren

Na wenn Du aus dem Array einen String nimmst, dann ist das keine Referenz mehr. Nur das Array an für sich.
Gut, vielleicht ungenau ausgedrückt aber üblicherweise arbeitet man ja mit den Elementen aus einem Array, oder? 😉

Und natürlich wird bei einem String bei einer Übergabe der Wert übergeben und nicht die Referenz.

49.485 Beiträge seit 2005
vor 9 Jahren

Hallo Abt,

bezüglich der Wertübergabe von Strings irrst du, siehe [FAQ] Besonderheiten der String-Klasse (immutabler Referenztyp mit Wertsemantik). Strings werden per Referenz übergeben. Der Wert des Strings wird dabei nicht kopiert/dupliziert. Ob nun ein String in einem Array steckt oder direkt übergeben wird, ändert daran nichts.

herbivore

16.806 Beiträge seit 2008
vor 9 Jahren

Das spielt doch keine Rolle bzw. ist hier doch immutable im Spiel.

Wenn Du einen String an eine Methode "Foo" übergibst und diese behält diese Information, dann kannst Du mit der Quell-Variable machen was Du willst.
Du kannst sie überschreiben oder nullen: in der Methode Foo bleibt's erhalten.

Anders wäre es eben mit einer "echten" Klassen.
Dass String ne class ist, ist klar. Aber ich würde sie nicht als "echte" bezeichnen, da sie sich eher wie ein WertTyp anfühlt.

49.485 Beiträge seit 2005
vor 9 Jahren

Hallo Abt,

korrekt, solange noch eine (aktive) Referenz auf das Objekt besteht, wird es nicht weggeräumt, so hatte ich es ja schon oben geschrieben, nur wird der Speicher eben nicht verdoppelt.

In dem Fall ging es ja um ein Array, das nicht nur ein Referenztyp ist, sondern sich auch so anfühlt.

Darin, dass String eine spezielle Klasse ist, stimmen wir überein. Dass sich String nicht wie ein Referenztyp anfühlt, bedeutet aber gerade nicht, dass hier auch tatsächlich Werte kopiert werden.

herbivore

16.806 Beiträge seit 2008
vor 9 Jahren

Ich seh das anders - oder wir reden aneinander vorbei.
Dass der String selbst ein unveränderlicher Referenztyp ist und, dass dann nur die Speicherposition übergeben wird: keine Frage.
Aber was ich meine zeig ich an diesem Beispiel:

Wenn er - und er zeigt es hier nicht - die Strings aus dem Array weiter verwendet und NICHT wieder freigibt (zB in einer Liste speichert) dann wächst der Speicher kontinuierlich.

  1. Deserialize erzeugt ein Array mit Strings
  2. die Strings verbleiben nicht im Array sondern werden weiter verarbeitet und bleiben im Speicher
  3. blocks Array wird neu gesetzt, altes Array geht verloren. Wir haben nun aber die (maximal) doppelte Anzahl an Strings.
class Program
{
    static String[ ] Deserialize()
    {
        return Enumerable.Range( 0, 1000 ).Select( i => "item" + i ).ToArray( );
    }

    static void Main( string[ ] args )
    {
        // Mehrfachen Deserialisieren simulieren
        foreach ( var i in Enumerable.Range( 0, 100000 ) )
        {
            var blocks = Deserialize( );

            foreach ( var item in blocks )
            {

                // Potenzielle Übergabe der Strings
                // immer wieder der gleiche Wert; aber wir behalten sie!
                Add( item );
            }
        }

        Console.WriteLine( "Do" );
        Console.ReadKey( );
    }

    static void Add( String item )
    {
        hold.Add( item );
    }

    static readonly List<String> hold = new List<string>( );
}

Durch die Situation, dass sie nicht im Array verbleiben, bleibt die Speicherposition in Nutzung.
Da kannst Du das Array auch 100 mal neue erstellen.

Das würde ich jetzt sagen, dass bei ihm passiert. Das würde erklären, wieso der Speicher bei jeder Durchlauf um 40MB steigt.

49.485 Beiträge seit 2005
vor 9 Jahren

Hallo Abt,

darin stimme ich dir zu.

Ich habe dir nur darin widersprochen, dass Strings als Werte übergeben werde, dass bei bzw. durch die Übergabe die Werte kopiert werden und dass (alleine) durch die Übergabe der Speicher(verbrauch) verdoppelt wird.

herbivore

16.806 Beiträge seit 2008
vor 9 Jahren

Ja, war undeutlich ausgedrückt.

849 Beiträge seit 2006
vor 9 Jahren

Hallo,

hört sich an wie ein Problem das ich mal in einem ähnlichen Context hatte.

Frage: Arbeitest Du mit angehängten Debugger? Wenn ja, probier das Program mal ohne zu starten.

Bei mir hat sich mein Programm auch immer mit GB von Speicher vollgezogen und ist am Ende mit einer OutOfMem ausgestiegen. Sobald ich aber den Debugger weggelassen habe, hat es sich zwar auch vollgezogen bis kein Speicher mehr da war, hat es dann aber korrekt mit einem mal abgeräumt. Um sich dann wieder voll zu saugen... Erklären konnte ich das damals auch nicht, vermute das waren irgendwelche Handles vom Studio.

Gruß

D
digi333 Themenstarter:in
290 Beiträge seit 2006
vor 9 Jahren

Die Antwort für mein Problem lag in einem kleinen Nebensatz von Abt.

die Variable "blocks" wird mit keiner anderen Methode weiter verarbeitet außer mit Array.Sort(). Die Funktion sieht jetzt so aus und der Speicher füllt sich zwar noch, aber beim erreichen des Maximums wird er relativ gut leer geräumt... Taskmanager sieht beim RAM dadurch aus wie Berg- und Talfahrt (das ist okay). Die Exception kommt erst mal nicht (aber das Programm brauch ja noch 5 Stunden).


public static void Deserialisiere<T>(out T objekt, string path)
        {
            BinaryFormatter binF = new BinaryFormatter();
            using (FileStream fs = new FileStream(path, FileMode.Open))
            {
                objekt = (T) binF.Deserialize(fs);
                fs.Close();
            }
        }

Den Profiler hab ich noch nicht verstanden. Ich dachte, er könnte mir ausgeben, welche Variable gerade wieviel Speicher benötigt.

16.806 Beiträge seit 2008
vor 9 Jahren

Es tut zwar nicht weh, aber wie bereits zwei Mal gesagt kannst Du Dir auch Close() sparen.
Intern ruft Dispose (bei den meisten Streams) Close() bereits auf und sorgt so für eine sichere Abhandlung.

Nich böse nehmen aber ich denke es tut Dir gut, wenn Du verstehst, was die jeweilige Methode tut - sonst ist das bisschen unnütz. Denke das ist hier der Grund des Fehlers 😉
Dafür gibt es auch die Doku:

Diese Methode gibt den Stream frei, indem die Änderungen in den Sicherungsspeicher geschrieben werden und der Stream geschlossen wird, um Ressourcen freizugeben.

G
497 Beiträge seit 2006
vor 9 Jahren

wobei es zwischen einem expliziten Dispose() und der Nutzung des FileStreams in einem using-Block eigentlich ja keinen Unterschied geben dürfte. using() ruft ja auch nur Dispose bei Verlassen des Blocks auf. Insofern hätte die ursprüngliche Lösung auch kein memoryleak erzeugen dürfen. Oder andersherum, das einzelne using() hätte ein evtl. vorhandenes Leak nicht beseitigen dürfen.

16.806 Beiträge seit 2008
vor 9 Jahren

using() ist ein Snippet für try/finally, das dafür sorgt, dass Ressourcen garantiert - auch bei einem Fehler - freigegeben werden.

Ich hätte bei der Fehlerbeschreibung hier nicht meine Hand ins Feuer gelegt, dass keine (weitere) Fehlermeldung existiert, die zB das Closen/Disposen verhindert und ein LEak ermöglicht hätte.
Deswegen hab ich mit Nachdruck darauf verwiesen 😉

P
157 Beiträge seit 2014
vor 9 Jahren

Die Sache mit dem Garbagecollector ist sone Sache...darauf sollte man sich nie verlassen. Der Grund wieso dein Ram voll läuft ist recht einfach erklärt : der gc räumt auf wenn er die Resourcen dafür bekommt oder er dazu gezwungen wird. Dadurch, dass du die Datei wahrscheinlich in einem Abwasch lädst, wirds natürlich Resourcentechnisch schwierig. Es gibt glaub auch sone Art Zeitstempel im GC, wann man das letzte mal ne Referenz angefasst hat, je länger das her ist, desto größer ist die Wahrscheinlichkeit, dass dein Objekt weg fliegt.

Der GC ist dafür da, dass man seinen reservierten Speicher nicht ständig freigeben muss, aber wenn man Systemresourcen belegt oder wie ein Fass ohne Boden Speicher reserviert, kann der einem auch nicht mehr helfen.

Ein GC.Collect ist ein Performancekiller schlecht hin und den sollte man nie aufrufen.

Wenn man Operationen mit großen Dateien durchführt, ist es weitaus sinnvoller in kleineren Blöcken zu arbeiten. Meist benötigt man nur einen Teil der Daten, die sich in der Datei befindet. Die Sache mit Release und Debug klappt nur solange, bis im Release ein gleichwertiger Speicherverbrauch erreicht wird.

Ein einfaches Beispiel, Videos,
Man kann für die Laden-Methoden einen Ringpuffer verwenden, dort werden so viele Bilddatenreingeschrieben (Haupt und Differenzialbilder) bis der Ringpuffer voll ist, beim Abspielen eines Frames werden die Bilddaten verworfen. Kombiniert mit einem Filepointer hat man so eine schnelle Leseoperation im RAM und einen Thread der den Ringpuffer im hintergrund mit Daten füttert.

Ich hatte vor ein paar Jahren (sind vielleicht ein paar mehr) 5mb - 5gb große Dateien aus Steuergeräten von Rennfahrzeugen, die sind in einen DX Oszilloskop in Echtzeit dargestellt worden sind. Wenn man mit solche Datenmengen arbeitet, kommt man ganz schnell an Speicher und Performancegrenzen, jenseits von gut und böse.

Man sollte auch immer beachten, dass die eigene Applikation nicht die Einzige auf dem System ist, daher immer versuchen kleine Päckchen zu schnüren...kostet am Ende auch weniger Zeit beim Debuggen, Review oder Refaktorieren 😉

PS : wer das Dispose vergisst, hat die ÄtschiBätschiException's verdient...

Wenn's zum weinen nicht reicht, lach drüber!

49.485 Beiträge seit 2005
vor 9 Jahren

Hallo Parso,

gut, dass du von deinen Erfahrungen mit großen Dateien berichtet hast. Einigen deiner Aussagen und Schlussfolgerungen kann ich jedoch nicht folgen. Grundsätzlich ist der GC schon verlässlich und in den allermeisten Fällen muss man sich um die Speicherverwaltung überhaupt nicht kümmern. Wir sprechen hier über einen Fall, der eine Ausnahme davon darstellt. Das bedeutet aber nicht, dass es generelle Probleme gäbe oder man sich ständig mit dem GC herumschlagen müsste. In den allermeisten Fällen ist es völlig ok, dass der GC den Speicher erst freigibt, wenn es einen Grund dafür gibt. Speicher ist dazu da, benutzt oder sogar ausgenutzt zu werden.

Natürlich sollte man sich der Grenzen der GC bewusst sein. Du sagst ganz richtig, dass der GC keine Systemressourcen freigibt (höchstens indirekt, wenn er verwaltete Objekte freigibt, die unverwaltete Systemressourcen benutzen und deren IDisposable korrekt implementiert ist). Der GC kümmert sich grundsätzlich nur um den Speicher der verwalteten Objekte. Eine OutOfMemoryException kann sich sowohl auf verwalteten als auch auf unverwalteten Speicher beziehen, so dass man aus einer solchen Exception nicht automatisch darauf schließen kann, der GC hätte seinen Job nicht gemacht. Eigentlich kann man das aus einer OutOfMemoryException sogar nie schließen.

Dateien in eine Rutsch (z.B. mit File.ReadAllBytes) zu lesen vereinfacht die Programmierung oft sehr und ist gleichzeitig deutlich weniger fehleranfällig, als eine blockweise Verarbeitung. Bei Dateien, bei denen es unmöglich oder zumindest verschwindend unwahrscheinlich ist, dass diese größer als der Hauptspeicher werden, würde ich mich daher immer nach dem KISS-Prinzip richten.

Bei (potenziell sehr) großen Dateien ist aber eine byte- oder blockweise Verarbeitung immer noch unvermeidlich, aber eben auch nur da. Und darin, dass man bei Verarbeitung solcher Dateien schnell Speicherprobleme bekommen kann, wenn man nicht aufpasst, stimme ich auch zu. Allerdings auch nicht unbedingt wegen des GCs, sondern eher trotz des GCs.

Was den Speicherbedarf anderer Anwendungen angeht, sollte man eine gewisse Kooperation zeigen, ohne dadurch die Vorteile der Speichernutzung für das eigene Programm zu verschenken. Es ist also nicht das Ziel, dass jedes Programm möglichst wenig Speicher benutzt. Es ist im Gegenteil das Ziel, dass alle Programme zusammen, den Speicher möglichst gut (aus)nutzen. Das dürfte im Normalfall dann gegeben sein, wenn jedes Programm Referenzen auf die Objekte hält, durch die es schneller arbeiten kann, ohne Referenzen auf Objekte zu halten, die schon nicht mehr benötigt werden. Den Rest macht dann schon der GC.

Darin, dass man Dispose nutzen und GC.Collect vermeiden sollte, stimme ich dir zu.

herbivore

W
872 Beiträge seit 2005
vor 9 Jahren

Vieles ist einfach besser geworden, seit man 64 Bit für Programme mit großen Dateien benutzen kann.
Ein 32 Bit Programm kann nicht 4 GB benutzen, sondern kracht häufig schon irgendwo zwischen 2.5 und 3 GB.