TList nie zwalnia pamięci poniżej pewnej wartości.

0

Zauważyłem dziwną rzecz i przegrzebawszy dokumentację oraz zagraniczne fora, nie jestem w stanie znaleźć odpowiedzi. Chodzi o to, że TList mimo prawidłowego zwolnienia pamięci (próbowałem różnych sposobów), nie chce wrócić do bazowej wartości zajmowanej pamięci.

Na przykładzie. Kiedy kompiluję pustą aplikację z formą, zajmuje ona w pamięci dokładnie 1.7 MB. Tworzę sobie listę na bazie TList i zapełniam ją obojętnie czym:

type TCustomList = class(TList<Integer>);

var
  CustomList: TCustomList;
  Value: Integer;

CustomList:= TCustomList.Create;
Value:= 3;
for I:= 0 to 10000 do CustomClass.Add(Value);

Teraz całość oczywiście zajmuje trochę więcej w pamięci, np. 2.4 MB. Zwalniam to tak:

CustomList.Clear;
CustomList.Free;

Niby działa... ale nie zawsze. To znaczy tylko wtedy, gdy ilość zajmowanej pamięci przekroczy 3.5 MB. Czyli np. zamiast 10000, dodaję 100000 elementów, wtedy zajmowana pamięć wynosi np. 100 MB. Zwalniam, a ilość zajmowanej pamięci spada do 3.5 MB. Próbowałem ustawiać CustomList.Capacity:= 0, wypełniałem też listę klasami i wskaźnikami, które indywidualnie zwalniałem i nadal nic, poniżej 3.5 MB TList za nic zejść nie chce. Efekt się nie "stackuje", bo gdy zrobię, wypełnię i potem zwolnię kilka TCustomList, to finalnie i tak zawsze zostaje 3.5 MB. Jeżeli ilość zajmowanej pamięci nie przekroczy tej wartości (i np. będzie na poziomie 2.9), to pamięć też się nie zwolni (a przynajmniej tak to wygląda).

Niby nic, ale jednak chciałbym wiedzieć dlaczego tak to działa, bo jakieś to dla mnie dziwne.

0

wygląda strasznie ale działa.

var
  MainHandle: THandle;

begin
  CustomList.Free;
  MainHandle := OpenProcess(PROCESS_ALL_ACCESS, false, GetCurrentProcessID);
  SetProcessWorkingSetSize(MainHandle, DWORD(-1), DWORD(-1));
  CloseHandle(MainHandle);
end;
0

Acha, związane jest to z czymś co nazywa się Delphi Memory Manager a dokładnie ze stertą oraz defragmentacją pamięci.

I jeszcze jedno, nie jest zalecane używanie procedury z poprzedniego postu.

2

Z tego co pamiętam TList w odróżnieniu od TObjectList nie jest właścicielem dodanych obiektów i nie zwolni ich wywołanie metody Clear trzeba w pętli przed zwolnieniem listy zwolnić każdy.
Zresztą sami piszą http://docs.embarcadero.com/products/rad_studio/delphiAndcpp2009/HelpUpdate2/EN/html/delphivclwin32/Generics_Collections_TList.html

0
kAzek napisał(a):

Z tego co pamiętam TList w odróżnieniu od TObjectList nie jest właścicielem dodanych obiektów i nie zwolni ich wywołanie metody Clear trzeba w pętli przed zwolnieniem listy zwolnić każdy.
Zresztą sami piszą http://docs.embarcadero.com/products/rad_studio/delphiAndcpp2009/HelpUpdate2/EN/html/delphivclwin32/Generics_Collections_TList.html

No właśnie też znalazłem taką informację, ale coś mi się nie zgadzało z tym co widziałem na ekranie. Według menadżera zadań po wywołaniu Clear pamięć była zwalniana (bo zużycie spadało). Niemniej - jeżeli dobrze rozumiem - to działa tak, że moja aplikacja niejako "porzuca" ten obszar pamięci (stąd menadżer pokazuje zmniejszoną ilość pamięci zajmowanej przez mój program), jednak te śmieci, które w niej umieściłem, nadal w niej pozostają, tak?

Czy zatem taki sposób będzie właściwy?

type
  PRec = ^TRec
  TRec = record
    Value: Integer;
  end;

  TCustomList = class(TList<PRec>);


var
  CustomList: TCustomList;

procedure Wypelnij;
var
  I: Integer;
  Rec: PRec;
begin
  CustomList:= TCustomList.Create;
  for I:= 0 to 10000 do 
    begin
      New(Rec);
      Rec.Value:= I;
      CustomList.Add(Rec);
    end;
end;

procedure Zwolnij;
var
  I: Integer;
begin
  for I:= 0 to CustomList.Count - 1 do Dispose(CustomList[I]);
  CustomList.Clear; //Według dokumentacji czyści miejsce po wpisach na liście i jest równoznaczne z ustawieniem Count i Capacity na 0
  CustomList.Free;
end;

Czy to zapobiegnie memleakom? Efekt pokazywany przez menadżer zadań nadal jest taki sam, tj. po zwolnieniu ciągle zajęte jest 3.5 MB pamięci. Próbowałem też przed dodawaniem jakiegokolwiek elementu do listy, ustawiać jej odpowiednie Capacity, żeby od razu zarezerwowała odpowiedni fragment pamięci (bez konieczności późniejszej relokacji), ale efekt jest ten sam, tj. ciągle zostają te nieszczęsne 3.5 MB po zwolnieniu.

Wiem, że teoretycznie mógłbym użyć TObjectList, ale to koniecznie musi być lista rekordów, nie klas (bo chcę korzystać z przeładowania operatorów, co nie działa w klasach).

1

Zastanawiałeś się kiedyś, jak to się dzieje, że aplikacja "dostaje" RAM?

Otóż chcąc otrzymać pewien kawałek pamięci, aplikacja musi odwołać się do kernela (poprzez tzw. syscall) i powiedzieć mu np. kce 128 MB - kernel przydziela wtedy pamięć i zwraca aplikacji odpowiedni wskaźnik.

Syscalle są wolne, dlatego podczas zwalniania pamięci aplikacja może zdecydować, że nie chce tego kawałka oddawać z powrotem systemowi w tej chwili, tylko chce zatrzymać go sobie "w razie gdyby się przydał za moment" - jest to taka forma recyklingu; dzięki temu sytuacje w stylu alokacja 2 MB + zwolnienie 2 MB + alokacja 2 MB + zwolnienie 2 MB + ... odwołują się do kernela tylko raz / dwa (na początku i na końcu operacji): jest to technika optymalizacyjna.

Gdy aplikacja stwierdzi, że dany fragment pamięci faktycznie nie jest już potrzebny, odda go z powrotem systemowi.

Innymi słowy - alokacja pamięci następuje w dwóch miejscach;

  • w kernelu,
  • w aplikacji.

Aplikacja (mimo jawnego wywołania obj.free();) może zdecydować, że nie chce jeszcze danego kawałka oddawać systemowi i może zatrzymać go dla siebie - jest to naturalne zachowanie, nie stanowi wycieku pamięci (poza sytuacjami, kiedy faktycznie spowodujesz wyciek poprzez np. niewyczyszczenie zawartości listy w sytuacji, kiedy jest to wymagane) i nie powinieneś w ten proces ingerować.

Podobnie działają domyślne alokatory JVM, C++ czy Rust (przynajmniej wtedy, gdy jeszcze wykorzystywał jemalloc - nie jestem pewien jak teraz).

Disclaimer: opisywana przeze mnie technika jest w całości up to the compiler. FPC oraz Delphi mają zupełnie różne alokatory pamięci (które możesz modyfikować btw, patrz: FastMM), które mogą zupełnie inaczej decydować o zarządzaniu RAMem wewnątrz aplikacji. Póki nie stanowi to poważnego problemu, nie powinno Cię interesować to, że odpaliłem free(), a te 512 kb nie zostało oddane kernelowi! - pamiętaj, że ma to na celu przyśpieszyć działanie aplikacji.

Disclaimer 2: poza sytuacjami, w którymi faktycznie masz wyciek.

0
Crow napisał(a):

/ciach/

Niby nic, ale jednak chciałbym wiedzieć dlaczego tak to działa, bo jakieś to dla mnie dziwne.

Po czym wnosisz, że pamieć nie jest zwolniona?
Sprawdzałeś wycieki inaczej niż przez nie wiem co - process managera?

0

Nie, patrzyłem tylko na menadżera. A po czym wnoszę? No właśnie nie wnoszę, tylko zastanawiam się, czy wszystko robię dobrze...

A jeżeli wypełnię TList np. rekordami (nie wskaźnikami na rekordy), albo prostymi typami jak Integer czy Double, to jak powinna wyglądać procedura zwalniania pamięci? Samo Clear wtedy wystarczy?

3
Crow napisał(a):

A jeżeli wypełnię TList np. rekordami (nie wskaźnikami na rekordy), albo prostymi typami jak Integer czy Double, to jak powinna wyglądać procedura zwalniania pamięci? Samo Clear wtedy wystarczy?

Wystarczy, bo wszystkie wymienione typy danych są zarządzane.

Jeżeli używasz listy przechowujacej dane zarządzane, to niczego nie musisz ręcznie czyścić. Jeśli używasz danych niezarządzanych, to lista jedynie pozbędzie się wskaźników na dane, ale nie zwolni samych danych, więc jeśli sam tego nie zrobisz to spowodujesz wyciek pamięci.

Natomiast dane w postaci instancji klas przechowuj w TObjectList, w konstruktorze podając True dla OwnsObjects. W takim przypadku lista będzie zwalniać instancje podczas ich usuwania (usunięcia pojedynczej pozycji lub wyczyszczenia całej listy). A jeśli sam chcesz się zająć zwalnianiem pamięci (powód nieistotny) to możesz albo podać False dla OwnsObjects, albo użyć TList – wyjdzie na to samo.

0

No to już wszystko rozumiem, dzięki wszystkim :).

1 użytkowników online, w tym zalogowanych: 0, gości: 1