Entwickler-Ecke
Delphi Language (Object-Pascal) / CLX - Dynamisches Array: SetLength kostet RAM? Mit Beispielprogr.
worm - Mi 18.12.02 18:01
Titel: Dynamisches Array: SetLength kostet RAM? Mit Beispielprogr.
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!
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; |
AndyB - 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:
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.
worm - 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.
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!