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: Stringvar 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!

user profile iconAScomp hat folgendes geschrieben Zum zitierten Posting springen:
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:
  1. LogData: TLogData
  2. fProtList: TStringList;
  3. 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:
user profile iconGausi hat folgendes geschrieben Zum zitierten Posting springen:
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: Stringvar 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: Stringvar 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!

user profile iconAScomp hat folgendes geschrieben Zum zitierten Posting springen:
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. ;-)