Autor Beitrag
AScomp
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Beiträge: 162


Delphi 5, Delphi 7, Delphi 2007, Delphi 2009, Delphi XE, Delphi 10 Seattle
BeitragVerfasst: Mi 07.09.11 12:50 
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:

ausblenden 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
ontopic starontopic starontopic starontopic starontopic starontopic starofftopic starofftopic star
Beiträge: 8548
Erhaltene Danke: 477

Windows 7, Windows 10
D7 PE, Delphi XE3 Prof, Delphi 10.3 CE
BeitragVerfasst: 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
ausblenden 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.

_________________
We are, we were and will not be.
Narses
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Administrator
Beiträge: 10183
Erhaltene Danke: 1256

W10ent
TP3 .. D7pro .. D10.2CE
BeitragVerfasst: 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

_________________
There are 10 types of people - those who understand binary and those who don´t.
AScomp Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Beiträge: 162


Delphi 5, Delphi 7, Delphi 2007, Delphi 2009, Delphi XE, Delphi 10 Seattle
BeitragVerfasst: 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 Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Beiträge: 162


Delphi 5, Delphi 7, Delphi 2007, Delphi 2009, Delphi XE, Delphi 10 Seattle
BeitragVerfasst: 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:

ausblenden 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:

ausblenden 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
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starofftopic star
Beiträge: 19314
Erhaltene Danke: 1747

W11 x64 (Chrome, Edge)
Delphi 11 Pro, Oxygene, C# (VS 2022), JS/HTML, Java (NB), PHP, Lazarus
BeitragVerfasst: 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 Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Beiträge: 162


Delphi 5, Delphi 7, Delphi 2007, Delphi 2009, Delphi XE, Delphi 10 Seattle
BeitragVerfasst: 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
ontopic starontopic starontopic starontopic starontopic starontopic starofftopic starofftopic star
Beiträge: 8548
Erhaltene Danke: 477

Windows 7, Windows 10
D7 PE, Delphi XE3 Prof, Delphi 10.3 CE
BeitragVerfasst: 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. ;-)

_________________
We are, we were and will not be.
Narses
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Administrator
Beiträge: 10183
Erhaltene Danke: 1256

W10ent
TP3 .. D7pro .. D10.2CE
BeitragVerfasst: 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

_________________
There are 10 types of people - those who understand binary and those who don´t.
AScomp Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Beiträge: 162


Delphi 5, Delphi 7, Delphi 2007, Delphi 2009, Delphi XE, Delphi 10 Seattle
BeitragVerfasst: 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. ;-)