Speicherbedarf eines Datasets in den Griff bekommen / Speicher wieder freigeben

  • C#
  • .NET (FX) 4.5–4.8

Es gibt 21 Antworten in diesem Thema. Der letzte Beitrag () ist von OlafSt.

    Speicherbedarf eines Datasets in den Griff bekommen / Speicher wieder freigeben

    Hallo Freunde,

    ich bekomme hier langsam ernste Speicherprobleme. Folgender Zehnzeiler sollte das Problem aufzeigen:

    C#-Quellcode

    1. SqlConnection Conn = new SqlConnection(ConnStr);
    2. SqlCommand cmd = new SqlCommand("SELECT Lat,Long FROM ResolverCache", Conn); //2xInt32
    3. SqlCommand cmd2 = new SqlCommand("SELECT BGrad,BMin,BSek,BDir,LGrad,LMin,LSek,LDir FROM GPS", Conn); //4xInt32, 2xChar(1), 2xDouble
    4. SqlDataAdapter da = new SqlDataAdapter(cmd);
    5. SqlDataAdapter da2 = new SqlDataAdapter(cmd2);
    6. DataSet ds = new DataSet();
    7. da.Fill(ds, "Cache"));
    8. da2.Fill(ds, "GPS"));


    Aus der Tabelle resolverCache werden nun knapp 400k Datensätze gefischt, aus der Tabelle GPS gute 5Mio. Das ganze landet, wie bei Datasets üblich, im Speicher, wodurch mein kleines Routinchen da oben satte 1,4GB an Speicher belegt.

    Anschließend werden die beiden Tabellen miteinander abgeglichen, aber dies ist für das Problem irrelevant.

    Mir stellen sich nun zwei Fragen:

    1.) Wenn mein Abgleich beendet ist, wie bekomme ich den Speicher wieder frei ? Ein erneute Aufruf meiner Abgöleichsroutine führt leider direkt in eine OutOfMemory-Exception.
    2.) Gibt es eine Möglichkeit, das ganze etwas Speicherfreudlicher zu gestalten ? Bei 10 Mio. Datensätzen wird das ganze echt kniffelig und bei 100Mio entspannt sich das auch nicht gerade...

    Beendet wird das ganze dann so:

    C#-Quellcode

    1. ds.Tables["Cache"].Clear();
    2. ds.Tables["GPS"].Clear();
    3. Conn.Close();


    Aber das gibt den Speicher nicht frei. Ich habe auch mit Dispose herumexperimentiert, ebenfalls ohne sichtbaren Erfolg.

    Was kann ich tun ?
    Bei solchen Datenmengen würde ich niemals alle Daten ins DataSet laden.
    Die benötigst du doch eh nicht alle.

    ​Anschließend werden die beiden Tabellen miteinander abgeglichen, aber dies ist für das Problem irrelevant.


    Offenbar ist dies ja der einzige Zweck, dass du alle Daten haben willst? Dann mach das Datenbankseitig und hol dir nur die Daten ins Frontend die man auch dem User anzeigen muss.

    LG
    Das ist meine Signatur und sie wird wunderbar sein!
    Doch, ich benötige die alle. Schließlich muß ich die Tabellen gegeneinander abgleichen. Die werden auch nirgendwo angezeigt oder von irgendwem betrachtet, das ist ein im Hintergrund ablaufender, vollautomatischer Prozeß.

    Noch jemand mit einer Idee ?
    jo, und ich muss nochmal nachfragen, eh ich da jetzt fett Tests unternehme:

    Nach deiner Aussage bleibt Speicher dauerhaft belegt durch ein (stark befülltes) Dataset, auch nachdem man es disposed und evtl. GarbageCollected hat?
    Und wenn man das mehrmals macht endet man in einer OutOfMemory-Exception?

    Das wäre ein ziemlich schlimmer Verstoß gegen den Dispose-Pattern, den MS dann da eingebaut hätte.

    Schon möglich, aber kann nicht auch sein, dass du in deiner Abgleich-Routine iwo ein Bug hast?
    Oder den Datenabruf nicht ordentlich aufräumst, dass da vlt. Resourcen belegt bleiben?
    @OlafSt
    Bei diesem Datenabgleich handelt es sich also um einen Prozess ohne Benutzereingriff und ohne Darstellung.
    Vielleicht sind aber dennoch die Mechanismen und Ideen, die hinter der DataGridView.VirtualMode-Eigenschaft liegen ein Lösungshinweis.

    ErfinderDesRades schrieb:

    ein ziemlich schlimmer Verstoß

    Ja, ohne Code kann man da nix sagen.

    Du könntest statt mit TableAdapern auch mit DbDataAdaperrn arbeiten - mache ich so bei mehreren Millionen Datensätzen bei einer IBM-DB2-Datenbank bzw. auch Microsoft-SQL-Datenbank! msdn.microsoft.com/en-us/library/kxs7kbfe(v=vs.110).aspx

    Da git es dann Start und MaxRecords - fünktioniert also ähnlich (und auch schnell!) wie z.B. TOP 10000 - nur dass noch der Start-Record angegeben werden kann.

    Ergibt ungefähr folgenden Code

    VB.NET-Quellcode

    1. dbadapter.Fill(dataSet, 0, 99999, "Table")
    2. 'mit Daten arbeiten
    3. Table.Clear
    4. dbadapter.Fill(dataSet, 100000, 199999, "Table")
    5. ' mit Daten arbeiten
    6. Table.Clear
    7. ' usw


    Ein Beispiel für DbDataAdapter findest Du auch unter : Sqlite
    Oder mit Hilfe von Google weitere Beispiele im Netz.

    Dein SQl-Select-Code muss natürlich bei der Sortierung mit Hilfe eines definierten Index oder Primary-Keys auf die Daten zugreifen!

    Edit:

    OlafSt schrieb:


    Anschließend werden die beiden Tabellen miteinander abgeglichen, aber dies ist für das Problem irrelevant.

    Ist die Frage ob dies irrelevant ist - je nach erforderlichem Abgleich, lassen sich da evtl. die einzulesenden Daten optimieren!

    Dieser Beitrag wurde bereits 8 mal editiert, zuletzt von „Thias“ ()

    ErfinderDesRades schrieb:

    ein ziemlich schlimmer Verstoß
    Hab jetzt Tests angefangen, und es ist wirklich so:
    Ein Dataset baut sich einen Speicher-Cache auf, und gibt den nicht wieder frei, nichtmal wenns Disposed, genullt und garbagecollected wird.
    ;(

    Also um riesige Datenmengen temporär zu verarbeiten und anschl. wieder freizugeben ist Dataset nicht verwendbar.
    Und man sollte wirklich immer nur ein Dataset pro Anwendung betreiben.
    Sorry - leider muss ich mich ein wenig korrigieren.

    Ein ordentliches Laufzeitverhalten bei mehr als 1 Mio Datensätzen mit:

    VB.NET-Quellcode

    1. Public Function Fill (
    2. dataSet As DataSet,
    3. startRecord As Integer,
    4. maxRecords As Integer,
    5. srcTable As String
    6. ) As Integer

    bekomme ich nur mit einer IBM-DB2-Datenbank-Tabelle hin.
    Die MS-SQL-Datenbank-Tabelle bei der ich selektiv einlese, hat doch "nur" ca. 100.000 Datensätze (da könnte man auch fast alle Datensätze einlesen).
    Eine andere MS-SQL-Tabelle mit ca. 10 Mio Datensätzen lässt sich mit dieser Methode bei mir nicht vernünftig bearbeiten - da scheint es einen internen Unterschied zu geben.

    @ErfinderDesRades Kannst Dein "hilfreich" also wieder entfernen!

    Edit: Gefühlsmäßig würde ich sagen, dass bei Tabel.Clear es manchmal bis zu 3 Minuten dauert bis GC "anspringt".

    Wenn ich im Task-Manger die Speicherbelgung des entsprechendes Thread betrachte, dann dauert es bei mir sehr lange bis der dem Thread zugeteilte Arbeitsspeicher wirklich wieder freigegeben ist.

    Dass gar nicht freigeben wird, dies kann ich so nicht bestätigen.

    Dieser Beitrag wurde bereits 3 mal editiert, zuletzt von „Thias“ ()

    VB.NET-Quellcode

    1. SqlCommand cmd = new SqlCommand("SELECT Lat,Long FROM ResolverCache", Conn); //2xInt32
    2. SqlCommand cmd2 = new SqlCommand("SELECT BGrad,BMin,BSek,BDir,LGrad,LMin,LSek,LDir FROM GPS", Conn); //4xInt32, 2xChar(1), 2xDouble

    Du hast Dich ja noch nicht dazu geäußerst, was Du da genau vergleichst.

    Aber für mich sieht dies danach aus, als ob Du irgendwie Koordinaten vergleichen / abgleichen wolltest.

    Sofern dies so ist, könntest Du dies nicht in mehre Teilaufgaben unterteilen, so dass Du mit einer (großzügigen) where-Clausel z.B. zuerst nur Koordinaten von Europa, dann Asien etc. einliest und verarbeitest?

    Edit: Die Farge von @Mono im Post 11 ist berechtigt - eine Antwort darauf kann aber nur gegeben werden, wenn bekannt ist, was da genau wie abgeglichen wird.

    Dieser Beitrag wurde bereits 4 mal editiert, zuletzt von „Thias“ ()

    Die Frage für mich bleibt. Warum wöllte man im Zweifel solche riesigen Datenmenge am Client im RAM vergleichen und aufbereiten wenn es am Datenbankserver vermutlich viel schneller und besser geht?
    Das ist meine Signatur und sie wird wunderbar sein!

    ErfinderDesRades schrieb:

    ein ziemlich schlimmer Verstoß
    Hab jetzt noch mehr getestet, und es ist doch nicht so:
    Also eine DataTable gibt ihren Speicher wieder frei, wenn man sie cleart.

    Dann kann man sie wohl auch für temporäre Verarbeitung großer Datenmengen nutzen.

    Weiß auch nicht, was ich da vorhin bei meine Q&D - Tests falsch gemacht hab.

    ErfinderDesRades schrieb:

    Weiß auch nicht, was ich ... falsch gemacht hab.
    Den GC nicht in die richtige Stimmung gebracht ;)
    Sofern man nicht explizit GC.Collect() aufruft, räumt der GC irgendwann auf.
    Und ich vermute stark, dass die verfügbare Menge RAM da auch eine Rolle spielt, also je mehr RAM man insgesamt hat, desto seltener wird aufgeräumt.
    Wozu sollte man auch die Anwendung blockieren, wenn noch mehr als genügend RAM übrig ist. Da wird dann wohl eher darauf gewartet, dass die Anwendung mal nichts macht.
    Ist aber nur eine These meinerseits, und muss nicht mit dem realen Verhalten des GCs übereinstimmen.
    Ohh - Mann - dies hatte ich sogar gelesen - mich aber als Anfänger vor ca. einem Jahr brav an das "sonst nicht machen" gehalten, dabei ist dies bei DataSets mit 1GB RAM mehr als hilfreich!

    Löffelmann S. 475

    VB.NET-Quellcode

    1. 'Nur zum Testen, sonst nicht machen!
    2. GC.Collect()


    Edit: Allerdings würde ich heutzutage meine Anwendung auch so programmieren, dass weniger Datensätze auf einmal geladen wären.

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „Thias“ ()

    Thias schrieb:

    mich aber als Anfänger vor ca. einem Jahr brav an das "sonst nicht machen" gehalten
    Auch als Fortgeschrittener oder "Profi" sollte man GC.Collect() so selten wie nur möglich verwenden. Es gibt nur sehr seltene Fälle in denen man den GC tatsächlich anstoßen muss. Sofern du keine OutOfMemoryException oder ähnliches bekommst, lässt du den GC brav seine Arbeit machen. Und selbst wenn es mal dazu kommt, solltest du eher mal in deinem Code danach ausschau halten, ob du auch ja alle möglichen .Dispose() Methoden aufgerufen hast, oder du nicht doch irgendwo noch eine Referenz auf ne Liste hast, die die dadurch, mit all ihren Objekten am Leben bleibt.

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „EaranMaleasi“ ()

    Naja - abgesehen davon, dass ich das Programm heutzutage strukturell komplett anders aufbauen würde, habe ich in dem Programm eine Pause bzw. eine noch "dümmere" Schleife bis ich weitere Daten einlese, um eben genau diese OutOfMemoryException zu vermeiden.

    Gleiches Problem wie bei @OlafSt bzgl. Table.Clear - aber nach Table.Clear ein "GC.Collect" und es können sofort in einem Rutsch wieder 1 Millionen-Datensätze eingelesen werden, ohne diese "Pause"

    Kann natürlich sein, dass da wegen strukturellen Schwächen, noch aus anderen Gründen "zu spät" aufgeräumt wird - muss ich wirklich mal überarbeiten mein Programm!

    Dehalb ist für mich die Frage von @Mono immer noch berechtigt, ob auch bei OlafSt seinem Programm wirklich alle Daten aufeinmal eingelesen werden müssen
    Da hier alle unbedingt wissen wollen, was das alles soll: Aus der GPS-Tabelle muß man aus BGrad, BMin und BSek sowie dem Char BDir eine Breitengradkoordinate machen. Desgleichen mit LGrad, LMin, LSek und dem Char LDir für die Längengrade. Diese nun gewonnene GPS-Koordinate wird in der Resolvertabelle gesucht und wenn gefunden, gehts zum nächsten Eintrag in der GPS-Tabelle.
    Findet sich nichts im Resolver, wird die Koordinate vermerkt und in einem folgenden Durchgang weiterverarbeitet - weshalb der Verarbeitungsprozess auch völlig irrelevant für dieses Problem ist. Es gibt auch keine DatagridViews oder ähnliches. Nur Dataset (nur eines !), TableAdapters, SqlCommands und eine SqlConnection. Kein einziges visuelles Control, kein Databinding in irgendeiner Form.

    Leider kann ich an der GPS-Tabelle keine Änderungen vornehmen (sonst hätte ich ein Feld "Resolved" schon drangehängt und entsprechend das SQL-Statement angepaßt. Auch ich habe keine Lust, wieder und wieder n Mio Datensätze zu überprüfen, obwohl das schon etliche Male gemacht wurde), das Design stammt nicht von mir. Ergo bin ich also gezwungen, immer wieder aufs neue die Millionen an Datensätzen der GPS-Tabelle einzulesen und zu prüfen. Anschließend bleiben, je nach Fahrbetrieb, vielleicht noch 10k-50k Koordinaten übrig, die noch weiterverarbeitet werden müssen - die spielen da keine ernstzunehmende Rolle in Sachen Speicherverbrauch.

    Es ist auch nicht möglich, die Daten "vorzufiltern". Ich muß ohnehin JEDE Koordinate bearbeiten. Ob diese Koordinate in Deutschland oder Italien oder der Schweiz liegt, stellt sich erst im Weiterverarbeitungsprozess heraus. Und: 99,99% der Koords sind eh in Deutschland. Die einzige sinnvolle Unterteilung wäre "Gib mir BGrad=1", "BGrad=2" usw. Doch wird dies früher oder später zum gleichen Problem führen, wenn das ganze sechs, sieben Jahre gelaufen ist.

    Nun. Ich habe erstens keinen Schimmer, wie man die ganze Rechnerei mit BGrad etc. in SQL erledigen soll. Zweitens möchte ich diese Last nur höchst ungern dem DB-Server auflasten, der hat mit dem Produktivbetrieb der Spedition schon genug am Hals. DIe Buchhaltung, Dispo und Geschäftsleitung steigen mir aufs Dach, wenn ich den Server mit sowas blockiere. Die eigentliche Weiterbearbeitung der Restkoordinaten kann der Server dann eh nicht leisten (Aufrufe von REST-Services etc), so das das ganze eh nix für einen DB-Server ist.

    Darum eine Clientanwendung. Der Rechner ist kaum größer als ne Zigarettenschachtel und sein einziger Job ist eben diese Aktion. Starte ich nun diese Aktion auf dem mit 4GB ausgestatteten Rechner, dann füllt sich der Speicher rasant - kein Wunder. Läuft es ein zweites mal, füllt sich der Speicher bis zur Maximalgrenze, dann fliegt die OutOfMemory-Exception.

    Auf jeden Fall ist GC.Collect() doch mal ein Hinweis, dem werde ich noch mal nachgehen, vielleicht fegt die erzwungene Garbage Collection den Speicher wieder frei. Auch der Hinweis mit DbTableAdapters und dem "scheibchenweisen" Auslesen finde ich hochinteressant.

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „OlafSt“ ()

    OK - Du kannst den Datenbankaufbau aus diversen Gründen nicht ändern - das Problem kenne ich - würde evtl. eine zusätliche Hilfstabelle incl. Resolver helfen (aber klar dann doppelte Datenspeicherung bei 5 Mio Sätzen!).

    Eien View geht wahrscheinlich nicht weil die Koordinaten evtl. erst umgerechnet werden müssen??? - Wieder meine Frage nach einer Hilfstabelle - die Hilfstabelle kann ja auch nur die 400.000 umgerechneten Datensätze der Resolver-Tabelle enthalten.

    Sofern Du aber aus Berechtigungsgründen oder sonstigen Gründen keine View erstellen kannst, tja wirklich jammerschade!

    Weil auch wenn Du in 7 Jahren dies evtl. auf einen PC mit 16GB RAM laufen lässt, wirst Du dann ähnliche Probleme haben, falls Du dann 50 Mio-Datensätze haben solltest.

    Hast Du auch schon einnmal probiert (falls von der Programm-Logik her möglich) mit dem SQL-Reader zu arbeiten und Deine 5 Mio-Datensätze einzeln hintereinader abzuarbeiten?
    Dies schließt ja arbeiten mit typisiertem-DataSet (DataTable / DataRow) nicht aus! Edit: Siehe abgesehen von Google z.B.: stackoverflow.com/questions/18…rrent-row-of-a-datareader

    Dieser Beitrag wurde bereits 3 mal editiert, zuletzt von „Thias“ ()

    *kopfklatsch* DataReader, natürlich ! =O

    Prinzipiell ist mir schon klar, das ich früher oder später auch mit 128GB RAM das ganze nicht mehr beherrschen kann. Aber ich hatte gehofft, bis dahin eine optimalere Lösung gefunden zu haben...

    Die bisherige Programmlogik läßt sich problemlos auf einen DataReader umstellen, da ich eh linear Datensatz für Datensatz abarbeite. Ob der erst vom SQL-Server herangeholt werden muß oder schon im Speicher liegt, ist mir egal. Ich probiere das mal aus.