Entwickler-Ecke
Dateizugriff - EOutOfMemory: StringList.SaveToFile schlägt mit UTF8 fehl
AScomp - Mi 07.09.11 12:50
Titel: EOutOfMemory: StringList.SaveToFile schlägt mit UTF8 fehl
Hallo,
folgendes Problem macht mir derzeit zu Schaffen, vielleicht weiß jemand Rat.
Ich möchte den Inhalt einer TStringList (kann mehrere MB an Daten enthalten) in eine Datei mit UTF8-Codierung schreiben, damit Unicode-Zeichen korrekt gespeichert werden. Dabei kann es zu einem EOutOfMemory-Fehler in der Zeile "fProtList.SaveToFile(AFilename, TEncoding.UTF8);" kommen:
Delphi-Quelltext
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21:
| function SaveLogData(const AFilename: String; var LogData: TLogData): Boolean; var fProtList: TStringList; xInt: Integer; begin result := false; if LogData = nil then exit; QuickSort(LogData); try fProtList := TStringList.Create; for xInt := Low(LogData) to High(LogData) do begin fProtList.Add(LogData[xInt].szPath); fProtList.Add(IntToStr(LogData[xInt].lTime)); end; fProtList.SaveToFile(AFilename, TEncoding.UTF8); result := true; finally fProtList.Free; end; end; |
MadExcept:
exception class : EOutOfMemory
exception message : Zu wenig Arbeitsspeicher.
main thread ($96c):
0040773f +0013 bkmaker.exe System 2851 +0 @NewUnicodeString
0040796b +000b bkmaker.exe System 2851 +0 @UStrFromPWCharLen
004a6075 +0091 bkmaker.exe Classes TStrings.GetTextStr
004a678a +002e bkmaker.exe Classes TStrings.SaveToStream
004a6723 +0037 bkmaker.exe Classes TStrings.SaveToFile
008bd227 +009b bkmaker.exe UnitMain 580 +11 SaveLogData
Hat jemand eine Idee, woran das liegen könnte?
Herzlichen Dank und viele Grüße
Andy
Gausi - Mi 07.09.11 13:06
Kommt der Fehler beim Speichern, oder schon während des Einfügens? Probier mal, vor der Schleife die Kapazität der Liste passend zu setzen, also auf
Delphi-Quelltext
1:
| fProtList.Capacity := 2 * (High(LogData) - Low(LogData) + 1); |
Dann muss nur einmal ein zusammenhängender Speicherblock für das Array hinter der Liste reserviert werden, und nicht ständig ein neuer, größerer.
Narses - Mi 07.09.11 13:08
Moin!
AScomp hat folgendes geschrieben : |
Hat jemand eine Idee, woran das liegen könnte? |
Du hast zu wenig RAM? :zwinker: Spaß bei Seite. ;) Du hast bei diesem Ansatz die Daten drei mal im Speicher: :idea:
- LogData: TLogData
- fProtList: TStringList;
- 004a6075 +0091 bkmaker.exe Classes TStrings.GetTextStr
Weiterhin hast du das Erzeugen der StringListe im try-Block, das ist ein Fehler. Das Erzeugen des Objektes liefert im Fehlerfall eine Exception, aber es wird kein Objekt angelegt, also kannst du es im finally auch nicht wieder freigeben. Wenn du das Erzeugen auch kapseln willst, musst du noch ein weiteres try-except drum rum spendieren. :nixweiss:
Ansatz zur Lösung:
Statt eine Stringliste zu erstellen könntest du direkt einen TFileStream aufmachen und die LogData-Elemente (Array?) über einen temporären UTF8-String da rein schreiben. Das sollte Speicher sparen.
//EDIT:
Gausi hat folgendes geschrieben : |
Dann muss nur einmal ein zusammenhängender Speicherblock für das Array hinter der Liste reserviert werden, und nicht ständig ein neuer, größerer. |
Interessanter Ansatz, aber wird das Problem nicht lösen, vermute ich. ;) Grund: Der Verwaltungsteil einer Stringliste ist auch nur ein Array aus Doppel-Pointern, die Strings selbst liegen ja auf dem Heap. Wenn man die Stringliste in der Kapazität anpasst, wird nur der Pointer-Block neu alloziert, und das sollte nicht so krasse Effekte haben, zumal die Stringliste intern nicht 1-er Schritte beim Vergrößern macht. :idea:
cu
Narses
AScomp - Mi 07.09.11 13:45
Danke euch, wie von Narses beschrieben werde ich es probieren.
try..finally: Stimmt, TStringList.Create steht nicht im try-Block. Ich ging einfach davon aus, dass das Erstellen einer StringList prinzipiell immer funktioniert - aber natürlich kann selbst das schon scheitern.
AScomp - Mi 07.09.11 15:23
Hätte da gleich nochmal eine Frage.
Und zwar bin ich auf den TStreamWriter gestoßen, mit dem kann ich die Daten wunderschön und recht flott ohne StringList direkt in eine Datei schreiben mit UTF-8-Codierung:
Delphi-Quelltext
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21:
| function SaveLogData(const AFilename: String; var LogData: TLogData): Boolean; var Writer: TStreamWriter; xInt: Integer; begin result := false; if LogData = nil then exit; QuickSort(LogData);
Writer := TStreamWriter.Create(AFilename, false, TEncoding.UTF8); try for xInt := Low(LogData) to High(LogData) do begin Writer.WriteLine(LogData[xInt].szPath); Writer.WriteLine(IntToStr(LogData[xInt].lTime)); end; result := true; finally Writer.Free; end; end; |
Allerdings krieg ich das Einlesen nicht hin:
Delphi-Quelltext
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24:
| function LoadLogData(const AFilename: String; var LogData: TLogData): Boolean; var Reader: TStreamReader; szPath, lTime: String; xLength: Integer; begin result := false; xLength := 0;
if FileExists(AFilename) then begin Reader := TStreamReader.Create(AFilename, TEncoding.UTF8); try while not Reader.EndOfStream do begin SetLength(LogData, xLength); LogData[xLength].szPath := Reader.ReadLine; LogData[xLength].lTime := StrToInt(Reader.ReadLine); Inc(xLength); end; result := true; finally Reader.Free; end; end; end; |
Nach ein paar Aufrufen von SetLength in der while-Schleife kommt eine Zugriffsverletzung. Ich vermute, dass er Probleme damit hat, das Array ständig zu vergrößern. Da ich allerdings Abwärtskompatibilität benötige, kann ich nicht einfach den Count als erste Zeile in die Datei schreiben und somit SetLength nur einmal aufrufen (was fehlerfrei funktionieren würde, bereits getestet).
Hat mir dazu noch jemand einen Tipp?
Gruß
Andy
jaenicke - Mi 07.09.11 15:59
Setze die Größe nicht in Einzelschritten sondern z.B. immer um 100 hoch, je nach erwarteter Datenmenge. Das lässt sich ja an der Dateigröße abschätzen. Nach Möglichkeit sollte die Schätzung natürlich so sein, dass die Länge genau etwas höher oder gleich der realen Anzahl ist.
Auf die Weise reservierst du seltener neuen Speicher. Und am Ende setzt du die Länge dann auf die reale Größe.
AScomp - Mi 07.09.11 16:08
Das hatte ich jetzt testhalber ohnehin schon gemacht. Allerdings hatte ich gehofft, dass es noch eine elegantere Lösung gibt.
Hast du noch eine Idee, weshalb es beim ständigen Vergrößern des Arrays zu Zugriffsverletzungen kommt? Gibt es dafür eine plausible Erklärung oder ist es einfach eine zu häufige Speicherreservierung in zu kurzen Zeitabständen?
Gausi - Mi 07.09.11 16:13
Vielleicht solltest du auch zu Beginn die Länge auf 1 setzen, nicht auf 0. Ein Array der Länge 0 hat nämlich gar keinen Eintrag, auch nicht den Nullten.
Du schreibst also immer neben dein Array - das geht wohl eine Zeitlang gut, aber irgendwann knallts halt. ;-)
Narses - Mi 07.09.11 16:13
Moin!
AScomp hat folgendes geschrieben : |
Allerdings hatte ich gehofft, dass es noch eine elegantere Lösung gibt. |
Statt Array eine Linked-List nehmen? :nixweiss:
cu
Narses
AScomp - Mi 07.09.11 16:30
Danke Gausi, das war's!
-> SetLength(LogData, xLength + 1);
Manchmal sieht man den Wald vor lauter Bäumen nicht. Oder man verwechselt Index mit Count. ;-)
Entwickler-Ecke.de based on phpBB
Copyright 2002 - 2011 by Tino Teuber, Copyright 2011 - 2025 by Christian Stelzmann Alle Rechte vorbehalten.
Alle Beiträge stammen von dritten Personen und dürfen geltendes Recht nicht verletzen.
Entwickler-Ecke und die zugehörigen Webseiten distanzieren sich ausdrücklich von Fremdinhalten jeglicher Art!