Autor Beitrag
worm
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starofftopic star
Beiträge: 135


D6 Prof
BeitragVerfasst: Mi 18.12.02 18:01 
Hi!
OK, ich habe ein komisches Problem. Gelöst habe ich es zwar jetzt bekommen, aber ich verstehe trotzdem nicht, warum es überhaupt auftritt! Ich habe ein dynamisches Array, was mit Pointern auf Records gefüllt ist (oder mit Objekten, kommt auf's selbe raus).
So. Das Array soll jetzt nach und nach mit Daten gefüllt werden. Die Anzahl der Datensätze ist zu Anfang noch unbekannt, deshalb dachte ich, alle 16 Datensätze vergrößere ich mal das Array per SetLength und initialisiere die neuen Pointer mit Pointern auf neue Records (per New). Irgendwie hat mein Programm dann aber bei 100.000 Datensätzen 100 MB RAM benötigt, also 1 KB pro Datensatz, obwohl der Record-Typ ja ziemlich klein ist.
Nach langer Suche habe ich dann mal aus der 16 eine 100 gemacht, und plötzlich war der Speicherverbrauch viel geringer; bei 1000 ging er schon auf knapp 9 MB runter. Warum??
Ich habe unten ein Beispielprogramm, an dem man über zwei Konstanten steuern kann, nach wie vielen Datensätzen das Array vergrößert wird (GrowSize) und nach wie vielen Datensätzen neue Records erzeugt werden (ClaimSize).
Einfach eine neue Anwendung erzeugen, einen Button auf die Form setzen und folgenden implementation-Teil einfügen.
Gelöst habe ich das Problem ja jetzt (da das Array nur Pointer enthält, kann GrowSize ruhig sehr groß sein; stört nicht), aber ich würde gerne wissen, woran das liegt! Für jeden Hinweis bin ich sehr dankbar!

ausblenden volle Höhe 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:
implementation

type
  TSomeData = packed record
    s1: String;
    i1: Cardinal;
    i2, i3: Integer;
    i4, i5: LongInt;
    b1: Byte;
  end;

  PSomeData = ^TSomeData;

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
const
  ArrSize = 100000;
  GrowSize = 16;  //muss durch ClaimSize teilbar sein!
  ClaimSize = 1;
var
  TheData: array of PSomeData;
  index: Cardinal;

  procedure GrowArray;
  begin
    SetLength(TheData, High(TheData) + GrowSize+1);
  end;

  procedure ClaimNextBlock;
  var i: Integer;
  begin
    for i := index to index+ClaimSize-1 do
    New(TheData[i]);
  end;

begin
  for index := 0 to ArrSize-1 do
  begin
    If index mod GrowSize = 0 Then GrowArray;
    If index mod ClaimSize = 0 Then ClaimNextBlock;
    TheData[index].s1 := 'Kurzer Text.';
  end;
  ShowMessage('Jetzt im Taskmanager die Speicherbelegung angucken (Win2k).'#13#10+
              'Je nach Größe von GrowSize sind es 8-100MB.');
  for index := ArrSize-1 downto 0 do
    Dispose(TheData[index]);
end;

_________________
In the beginning, the universe was created. This has made a lot of people very angry, and is generally considered to have been a bad move.
AndyB
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 1173
Erhaltene Danke: 14


RAD Studio XE2
BeitragVerfasst: Do 19.12.02 20:45 
Der "Fehler" liegt in der Speicherverwaltung von Delphi. Diese liefert Zeiger, die in einen für Windows reservierten Bereich zeigen (was ja einleuchtet), nur zeigt in diesen einen riesigen Bereich nicht nur 1 Zeiger, sondern mehrere. Mit anderen Worten: einen Windows Speicherbereich teilt Delphi in mehrere eigene Speicherbereiche auf. Zu dem arbeitet Delphi mit freien Blöcken. Diese "freien" Blöcke werden erst wirklich freigegeben, wenn der gesamte Windows Speicherbereich als "frei" markiert ist.
Durch das viele GetMem und FreeMem (von SetLength ausgelöst) entstehen lauter Löcher, die jedoch nicht vom vergrößerten Array genutzt werden können, da sie eben gerade um GrowSize*SizeOf(Pointer) zu klein sind.

Zur Lösung dieses Speicherfressens, könnte man einen eigenen Speichernmanager einsetzen, der nur die Windowsfunktionen nutzt:
ausblenden volle Höhe 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:
unit WinMemMan;
interface
uses Windows;

implementation

function WinGetMem(Size: Integer): Pointer;
begin
  Result := GlobalAllocPtr(GMEM_MOVEABLE, Size);
end;

function WinFreeMem(P: Pointer): Integer;
begin
  Result := GlobalFreePtr(P);
end;

function WinReallocMem(P: Pointer; Size: Integer): Pointer;
begin
  Result := GlobalReAllocPtr(P, Size, GMEM_MOVEABLE);
end;


procedure InitializeWinMemManager;
var WinMemManager: TMemoryManager;
begin
  WinMemManager.GetMem := WinGetMem;
  WinMemManager.FreeMem := WinFreeMem;
  WinMemManager.ReallocMem := WinReallocMem;
  SetMemoryManager(WinMemManager);
end;

initialization
  InitializeWinMemManager;

end.

Das einzige Problem dabei ist, dass bei vielen (100000 Durchläufen) Windows die Speicherhandles ausgehen und EOutOfMemory ausgelöst wird.

_________________
Ist Zeit wirklich Geld?
worm Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starofftopic star
Beiträge: 135


D6 Prof
BeitragVerfasst: Do 19.12.02 23:26 
Hi und danke erstmal! Jetzt weiß ich zumindest, dass der Fehler nicht direkt an mir lag :).
Aber so ganz hab ich das noch nicht verstanden. SetLength verändert doch eigentlich nur die Größe des Arrays, und das Array besteht nur aus ein paar 32Bit-Werten. Davon, dass die Größe des Arrays verändert wird, sollten doch die Records gar nicht betroffen sein, oder? Lässt man nämlich das New(PSomeData) (und was da so zugehört) weg, gibt es keine Speicherprobleme mehr. Also was haben die Records da noch mit zu tun?

Kann es vielleicht sein, dass Delphi erst dem Array einen Platz zuweist, und dann den Records, die direkt danach erzeugt werden, den Platz direkt hinter dem Array erhalten? Also das erste Record wäre bei der Adresse "PSomeData(LongInt(@TheData)+4*(High(TheData)+1))"? Und dann soll irgendwann das Array vergrößert werden und plötzlich sind da ganz viele Records im Weg (nämlich genau GrowSize Stück)? Also wird der Speicher des Arrays freigegeben (aber so, dass dieser Speicher trotzdem nicht irgendwann mit neuen Records gefüllt wird) und das Array neu hinter dem jetzt letzten Record erzeugt ("LongInt(TheData[High(TheData)])+SizeOf(TSomeData)").
Ich weiß nicht, ob das alles so stimmt, aber für mich würde es Sinn machen. Es macht für mich dann bloß keinen Sinn, dass der freigewordene Platz nicht mit den nächsten Records gefüllt wird. Aber jedenfalls passt die "Theorie" zu dem Verhalten (bei größerer GrowSize muss das Record nicht so oft verschoben werden, also weniger Speicherverschwendung durch Fragmentierung).

Sagt mir mal einer, ob ich damit richtig liege und wenn ja, warum nicht (ok, der ist alt :oops:)? Würde schon gerne mal etwas mehr davon verstehen.

_________________
In the beginning, the universe was created. This has made a lot of people very angry, and is generally considered to have been a bad move.