Entwickler-Ecke

Algorithmen, Optimierung und Assembler - String-Array und Speicherverbrauch


Xion - Mi 26.01.11 17:21
Titel: String-Array und Speicherverbrauch
Hi,

folgende Situation (vereinfacht):

Delphi-Quelltext
1:
2:
3:
4:
5:
type TDataPack=record
  Name: String;
end;

List: array of TDataPack;


Wenn ich jetzt das array nach und nach vergrößere und fülle, steigt mein RAM-Verbrauch stark an (im Taskmanager guck ich). Bei 40.000 Strings bin ich bereits in einer Region von 350MB und dann krieg ich eine "Out of memory" Exception.

Um rauszukriegen was das ist, hab ich das array von vornherein mit 1Mio Einträgen initialisiert und überall einen elend langen String reingeschrieben -> 150MB verbrauch. Kaum fängt mein Programm an und ersetzt dort jetzt die langen Strings durch kurze, steigt der Verbrauch rasch an. Das fand ich schonmal sehr seltsam. Dafür gab es so keine "Out of memory" Exception, aber ich hab ihn dann bei 0.5GB doch abgebrochen ;)

Mehr oder weniger durch Zufall hab ich jetzt gemerkt:
Die Lösung ist, denn String gleich mit fester länge zu definieren:


Delphi-Quelltext
1:
2:
3:
type TDataPack=record
  Name: String[255];
end;

So bleibt der RAM-Verbrauch sehr stabil.

Was ich jetzt garnicht verstehe: Warum?
Ich habe jeweils jede Zelle nur 1x beschrieben. Wie kann es da zu einem so extremen RAM Anstieg kommen? Wenn der Effekt wäre, dass der Platz zu groß ist und jetzt in der array-struktur platz leer wäre, also lagert Delphi es aus und vergisst den großen Platz frei zu geben... Aber dann würde sich der Verbrauch ja maximal verdoppeln, und der Effekt wäre minimal wenn ich ein 150 Zeichen langen String durch einen 5 Zeichen langen ersetze. Und ich habe niemals nie einen größeren String reingeschrieben, als ich bei der Initialisierung hatte. Die 255 Zeichen ist viel zu viel für mich, doch brauch ich viel weniger Speicher :roll:

Wäre toll wenn mir das einer erklären kann ;)


Moderiert von user profile iconNarses: Topic aus Delphi Language (Object-Pascal) / CLX verschoben am Mi 26.01.2011 um 16:37


jaenicke - Mi 26.01.11 17:37

Dann hast du wahrscheinlich eine alte Delphiversion, oder? (Stimmt die im Profil?) Es gab da bis Delphi 2005 einige solcher Probleme, die aber ab Delphi 2006 sukzessive behoben wurden. Bei XE kann ich so etwas nicht reproduzieren, das Thema hatte ich schonmal.

Wie sieht denn dein Testcode aus?


Horst_H - Mi 26.01.11 17:45

Hallo,

Bei der Verarbeitung von Ansistrings kann man viele Zwischenergebnisse als "Speicherleichen" zurücklassen, weil die Referenzzählung nicht dahintergekommen ist, das Du diese nicht mehr verwendest.
Aber shortstring sind immer uniquestrings. Temporäre in Funktionen werden also immer nach verlassen gelöscht.
Die Auslagereung der Verarbeitung in eine Prozedur kann helfen:
http://www.delphi-forum.de/viewtopic.php?t=51490

Gruß Horst


Xion - Mi 26.01.11 18:18

user profile iconjaenicke hat folgendes geschrieben Zum zitierten Posting springen:
Dann hast du wahrscheinlich eine alte Delphiversion, oder? (Stimmt die im Profil?) Es gab da bis Delphi 2005 einige solcher Probleme

Hab Delphi 2005, ja.

user profile iconjaenicke hat folgendes geschrieben Zum zitierten Posting springen:
Wie sieht denn dein Testcode aus?


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
procedure TForm1.FormCreate(Sender: TObject);
var a: integer;
begin
  HashTable:=THashTable.Create(1000001);
  for A:= 0 to HashTable.TableSize-1 do
    HashTable.Insert(A,'1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890');
  for A:= 0 to HashTable.TableSize-1 do
    HashTable.delete(A);

  //die Tabelle ist also nach außen hin leer, die Strings bleiben aber gespeichert (damit ich lange Strings durch kurze ersetzen kann ;)
end;



In einem Timer
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
 
var WebData, SearchedWord: String;
    A: integer;
begin
{+}WebData:=http.Get('URL');
 for A:= 0 to 99 do
   begin
{+}     WebData:=Copy(WebData, Pos('ba',WebData) , MaxInt);
{+}     WebData:=Copy(WebData, Pos('q=',WebData)+2 , MaxInt);
{+}     SearchedWord:=AnsiLowerCase( UTF8ToANSi(httpdecode  ( Copy(WebData, 1, Pos('\',WebData)-1 )) ) );
     SearchedWord:=randomChar+randomChar+randomChar+randomChar+randomChar+randomChar;
     HashTable.Insert(GlobalWordCount,SearchedWord);
     GlobalWordCount:=GlobalWordCount+1;
     SearchedWord:='';
     WebData:='';
   end;
end;


Obwohl ich alles wegwerfe und dem SearchedWord einfach nur ne hand voll beliebige Chars zuweise, tritt der Effekt ein.
Lass ich aber die mit + markierten Zeilen weg, dann tritt der Effekt NICHT ein, dann sinkt sogar der Speicherverbrauch (es werden ja die langen durch kurze Strings ersetzt)...dabei tun die garnichts.


user profile iconHorst_H hat folgendes geschrieben Zum zitierten Posting springen:
Die Auslagereung der Verarbeitung in eine Prozedur kann helfen:
http://www.delphi-forum.de/viewtopic.php?t=51490

Hat auf Anhieb nichts gebracht. Muss ich nochmal später testen.


Tastaro - Mi 26.01.11 18:23


Delphi-Quelltext
1:
HashTable.Insert(A,'1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890');                    

Das sollte doch eigentlich immer den gleichen Hashwert erzeugen. Demzufolge wird deinen Hashtabelle nicht mit langen Strings vollgeschrieben, sondern nur Einträge bei dem einen Hashwert erzeugt.

Beste Grüße


Xion - Mi 26.01.11 18:27

user profile iconTastaro hat folgendes geschrieben Zum zitierten Posting springen:
Das sollte doch eigentlich immer den gleichen Hashwert erzeugen.

Hehe, das ist genau der Grund warum ich nicht mit Code angefangen hab, da kommen wir vom hundertsten ins tausendste.

Dazu: Die HashTable hab ich selbst programmiert und ich weiß wie sie funktioniert. Der erste Wert ist der Key, der zweite nur ein String dazu. Den String in einen Integer "hashen" mach ich vorher. Die Hashtabelle ist nur dafür da, integer in eine Tabelle zu klatschen und zu suchen usw. (Wörterbuch: Es gehört zu dem key immer ein String. Mit dem String wird aber nichts getan, außer ihn zu speichern)

Der Code stimmt. Der funktioniert so, und es werden auch IMMER lange durch kurze Strings ersetzt.


jaenicke - Mi 26.01.11 21:34

Ich meinte jetzt eher den Code dazu:
user profile iconXion hat folgendes geschrieben Zum zitierten Posting springen:
Um rauszukriegen was das ist, hab ich das array von vornherein mit 1Mio Einträgen initialisiert und überall einen elend langen String reingeschrieben -> 150MB verbrauch. Kaum fängt mein Programm an und ersetzt dort jetzt die langen Strings durch kurze, steigt der Verbrauch rasch an.
Denn wie gesagt, unter XE kann ich sowas nicht reproduzieren, deshalb wäre es interessant, ob das daran liegt, dass ich den Code anders (besser :P) geschrieben habe oder an der Delphiversion. ;-)


Xion - Mi 26.01.11 23:07

Es war zwar die betreffende Stelle, aber jetzt hab ich das ganze nochmal "extrahiert":



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:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
type TDataPack=record
  Name: String;
  Filled: boolean;
  OrderedNext: integer;
end;

var
  Form1: TForm1;
  S: array of TDataPack;
  http: TidHTTP;  
  BackUpWebData:String;

implementation

{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject);
var a: integer;
begin
            
  randomize;
  http:=TidHTTP.Create;

  SetLength(S,499999);
  for A:= 0 to Length(S)-1 do
    S[A].Name:='Ford und Arthur beschlossen, sich einfach zurück zu lehnen und entsetzt zu sein.';

end;


function randomChar: Char;
var x: integer;
begin
 x:=ord('z')-ord('a');
 Result:=chr(random(x)+ord('a'));
end;

procedure TForm1.Timer1Timer(Sender: TObject);
var WebData, SearchedWord: String;
    A, WordIndex: integer;

begin

  if (BackupWebData=''then
    begin
      WebData:=http.Get('http://www.sieglin.de/arne/primzahlen.html');
      BackupWebData:=WebData;
    end;
 for A:= 0 to 99 do
    begin
      WebData:=BackupWebData;

{+}    WebData:=Copy(WebData, Pos(' ',WebData) , MaxInt);
      SearchedWord:=randomChar+randomChar+randomChar+randomChar+randomChar+randomChar;

      WordIndex:=random(Length(S));
      S[WordIndex].Name:=SearchedWord;
      S[WordIndex].Filled:=True;

      WebData:='';

    end;
end;

Die mit + angemerkte Zeile entscheidet über Speicher Leak ja/nein.

Im Anhang ist das Projekt nochmal als ganzes.

PS: So ist das Leck auch weg:

Delphi-Quelltext
1:
2:
3:
4:
5:
type TDataPack=record
  Name: String[255];
  Filled: boolean;
  OrderedNext: integer;
end;


jaenicke - Do 27.01.11 00:11

Falls du Turbo Delphi hast, nimm einfach das, das Problem tritt ab der Version nicht mehr auf.

In den älteren Versionen tritt das Problem auf, ich habe es mit Delphi 7 und 2005 getestet. Aber u.a. der dort noch verwendete Speichermanager hatte eben diverse Probleme.

Du kannst es mit FastMM versuchen. Falls es nur ein Problem des Speichermanagers ist, könnte das helfen. Aber ich vermute tatsächlich ein Problem mit der Referenzzählung und dann hilft das nichts.

Was mir gerade auffällt: Du benutzt den String als Parameter und Rückgabewert für Copy. Der String, der der Rückgabewert ist, wird als zusätzlicher Parameter an Copy übergeben. Da das hier die selbe Variable ist, kann das das Problem sein, wenn die alten Delphiversionen dies nicht korrekt umsetzen.
Schonmal versucht BackupWebData direkt an Copy zu übergeben? (Ich probiere es gleich und schaue in den Assemblercode.)

// EDIT: Bringt leider nichts.


Martok - Do 27.01.11 00:16

Ich hatte ein ähnliches Problem mit einem Datenbanksystem unter Delphi 7.

FastMM hat das Problem vollständig behoben, von steigend ~150MB ist der Speicherverbrauch auf 6MB konstant gefallen.


jaenicke - Do 27.01.11 00:26

Ja, jetzt habe ich es getestet. In diesem Fall genügt es tatsächlich den Speichermanager durch FastMM zu ersetzen. Dann tritt das Problem nicht mehr auf.

(Was im Grunde auch logisch ist, da mit Delphi 2006 FastMM integriert wurde, deshalb klappt es ab da auch direkt.)


BenBE - Do 27.01.11 01:24

Der alte Speichermanager von Delphi hatte auch in Hinblick auf die Fragmentierung des Speichers arge Probleme. Daher sollte man (nicht nur aus Performance-Gründen) bei bekannter Länge eines dynamischen Arrays diese direkt setzen, bzw. größere Sprünge machen und "Präalloziieren".

Zum Speicherverbrauch:

Array of T: Length*SizeOf(T) + 4
T=Record ... End: Summe der Einzelfelder zzgl. Alignment (meist 4 Bytes)
String: (Länge+1) + 8 + Alignment (meist 4 Byte) + 4 Bytes Zeiger

Bei 1 Mio Einträgen mit Strings mit jeweils Länge 121 sind das also:
4 + 1000000*SizeOf(121+1+8+2+4) = 4+1000000*136=136000004 = 136MB+4Byte ;-)


Xion - Do 27.01.11 10:46

user profile iconjaenicke hat folgendes geschrieben Zum zitierten Posting springen:
Du kannst es mit FastMM versuchen. Falls es nur ein Problem des Speichermanagers ist, könnte das helfen.


Jo, das scheint zu klappen. Cool find ich ja, dass er mir beim Beenden des Programms noch sagt, wo ich überall noch Speicherleichen habe ;) Ich dachte immer, globale Variablen (wie das http: TIdhttp) werden automatisch gelöscht...hmm, eigentlich müssten sie das auch...

user profile iconBenBE hat folgendes geschrieben Zum zitierten Posting springen:
Daher sollte man (nicht nur aus Performance-Gründen) bei bekannter Länge eines dynamischen Arrays diese direkt setzen, bzw. größere Sprünge machen und "Präalloziieren".

Das hat ja nichts gebracht ;) Ich habe es ja am Anfang auf utopisch groß gesetzt und er hat trotzdem zusätlich Speicher gefressen, dabei war bereits mehr als genug alloziiert.

Danke für die Berechnung :zustimm:


Gausi - Do 27.01.11 10:53

user profile iconXion hat folgendes geschrieben Zum zitierten Posting springen:
Ich dachte immer, globale Variablen (wie das http: TIdhttp) werden automatisch gelöscht...hmm, eigentlich müssten sie das auch...


Nein. Was du selber erzeugst, muss du auch explizit selber wieder freigeben.


Xion - Do 27.01.11 10:55

Wieder was gelernt :mahn: