Garbage Collector, cz. II   

Udostępnij na: Facebook

Autor: Piotr Zieliński

Opublikowano: 2013-05-13

Wprowadzenie

Sposób działania metody Garbage Collector zależy od typu aplikacji. Jeśli chodzi o alokację pamięci, aplikacje klienckie mają inną specyfikę niż aplikacje serwerowe. Aby dany program płynnie wyświetlał interfejs użytkownika (animacje itp.), nie do zaakceptowania jest sytuacja, w której Garbage Collector wstrzymuje wszystkie wątki, aby zwolnić niepotrzebne zasoby. Wówczas, spowodowałoby to przerwanie wyświetlanej animacji, a użytkownik zorientowałby się, że nie ma do czynienia z „systemem czasu rzeczywistego”. Cześć aplikacji serwerowych, których zadaniem jest symulacja środowiska wykonywanego w czasie rzeczywistym (systemy Windows nie są systemami czasu rzeczywistego, więc mowa tutaj tylko o symulacji), nie może sobie pozwolić na żadne opóźnienia. Z drugiej strony, aplikacje webowe zwykle nie mają z tym problemu. Opóźnienie rzędu kilku milisekund jest niczym w porównaniu z opóźnieniami, które mają miejsce w różnych fragmentach infrastruktury sieciowej (routery, okablowanie, itp.). W takich przypadkach, lepiej zatrzymać wątki na jakiś czas i po prostu zwolnić pamięć. Jest to rozwiązanie najbardziej optymalne, gwarantujące największą przepustowość (co dla aplikacji webowych jest ważne), ale powoduje niestety krótką pauzę.

Typy GC

Garbage Collector może pracować w dwóch trybach, które są wybierane w momencie, gdy aplikacja jest uruchamiania. W czasie działania aplikacji zmiana ustawień jest już niemożliwa. Wspomniane tryby to:

  • Workstation – domyślne ustawienie, przeznaczone dla zwykłych komputerów (gdy na danej maszynie może być uruchomionych wiele procesów). W tym trybie GC stara się wykonywać operacje w taki sposób, aby nie blokować wątków przez dłuższy czas. Należy mieć na uwadze, że gdy GC jest uruchamiany to wszystkie wątki muszą zostać zatrzymane. W Workstation GC stara się robić to w bardzo krótkim czasie, aby aplikacje sprawiały wrażenie, że działają w czasie rzeczywistym. Ponadto, GC zakłada, że na komputerze uruchomione są inne aplikacje, dlatego nie będzie zbytnio obciążał CPU, aby nie blokować pozostałych procesów. Wątek, który spowoduje kolekcję (np. poprzez zadeklarowanie obiektu, dla którego nie ma wystarczająco dużo pamięci), odpowiedzialny jest zawieszenie pozostałych wątków i zwolnienie zasobów. Taki wątek staje się wątkiem GC.

GC w trybie Workstation

Rys. 1. GC w trybie Workstation.

Wynika z tego coś jeszcze. Skoro kolekcja wykonywana jest na wątku, który ją spowodował, priorytet będzie taki, jaki aktualnie miał wątek. Z tego powodu GC będzie musiał konkurować z pozostałymi wątkami w systemie podczas planowania (scheduling),

 

  • Server – tryb zoptymalizowany pod kątem aplikacji serwerowych. Sterta jest dzielona na kilka części, w zależności od liczby CPU. Następnie, podczas kolekcji tworzone są wątki dla każdego procesora. Takim sposobem kolekcja ma charakter współbieżny – każdy wątek\CPU odpowiedzialny jest za zwolnienie zasobów ze swojej sterty. W tym trybie GC przypuszcza, że nie ma innych aplikacji na danym komputerze, a co za tym idzie, procesor CPU może być wykorzystywany na wyłączność. Gdy dana maszyna posiada wyłącznie jeden rdzeń, wtedy zawsze wybierany jest Workstation. Kolekcja Server jest dużo szybsza, ale niestety również więcej zasobów pożera. Jeśli dany komputer ma 8 rdzeni i wykonuje 4 procesy, wtedy będzie trzeba stworzyć 32 wątki! Należy pamiętać, że wątki są bardzo dużym obciążeniem dla systemu. Z tego względu, korzystanie z trybu Server w środowiskach, w których uruchomionych jest bardzo wiele procesorów, przynosi zwykle negatywne skutki.

Garbage Collector w trybie serwer

Rys. 2. Garbage Collector w trybie serwer.

ASP.NET czy SQL Server, które hostują CLR, działają w trybie serwerowym (jeśli dany komputer posiada kilka rdzeni), co wydaje się naturalne. Aplikacje typowo klienckie można uruchomić również z GC w trybie Server, modyfikując plik konfiguracyjny:

<configuration>
<runtime>
<gcServer enabled="true"/>
</runtime>
</configuration>

Następnie, można sprawdzić w aplikacji, czy tryb serwerowy został faktycznie załadowany:

Console.WriteLine(GCSettings.IsServerGC);

Powyższe opisy dotyczą podstawowych implementacji, które są nierównoległe (za chwilę wyjaśnię, co to dokładnie znaczy). Oprócz tego, każdy z powyższych trybów może dodatkowo działać w sposób równoległy.

Może być to trochę mylące, ponieważ zgodnie z powyższym opisem, kolekcje typu serwer zawsze używają kilka wątków. Jest to oczywiście prawda, ale mówiąc „concurrent server mode” albo „concurrent workstation mode”, określa się wątek, który odpowiedzialny jest za przeszukiwanie grafu obiektów w celu znalezienia tych nieosiągalnych.

W trybach równoległych (dotyczy server i workstation) istnieje dedykowany wątek, który w tle będzie sprawdzał, które obiekty są nieosiągalne. Ma to miejsce w trakcie wykonywania pozostałych wątków – nie ma potrzeby zatrzymywania ich. Operacja jest całkowicie niezależna i nie wpływa na inne wątki, jak to ma miejsce w przypadku klasycznej kolekcji. W momencie, w którym deklaracja jakiegoś obiektu przekracza limit dostępnej pamięci, wszystkie wątki są wstrzymane (tutaj bez zmian), a następnie:

  1. gdy należy wykonać kolekcję GEN0 lub GEN1, wtedy algorytm jest identyczny (jak w wersji nierównoległej). Usunięcie pamięci z GEN0\GEN1 zajmuje niewiele czasu, zatem można pozwolić sobie na blokowanie innych wątków, jak to ma miejsce w rozwiązaniu nierównoległym,
  2. gdy należy wykonać kolekcję GEN2, wtedy budżet GEN0 jest zwiększany. Skutkuje to oczywiście tym, że można zadeklarować dany obiekt, który stworzył kolekcję. Po tym, wątki są wznawiane, a dodatkowy wątek zbiera informacje o nieosiągalnych obiektach, aby móc potem zawiesić wątki i zwolnić zidentyfikowane zasoby.

Z powyższych punktów wynika, że GC może zdecydować się na powiększenie budżetu, przy czym zasoby nie zostaną zwolnione. GC robi to dość często (jeśli oczywiście pamięć aktualnie nie jest zbyt obciążona), a przez to, aplikacje zarządzane przez równoległy GC pochłaniają zwykle więcej zasobów niż w przypadku rozwiązania czysto nierównoległego.

Dzięki współbieżnemu podejściu, czas, w którym wątki są zatrzymywane, jest dużo mniejszy, przez co aplikacje zachowują się bardziej płynnie.

Domyślnym typem dla Workstation jest concurrent, ale można to oczywiście konfigurować:

<configuration>
<runtime>
<gcConcurrent enabled="false"/>
</runtime>
</configuration>

Mechanizm “concurrent gc” został zastąpiony “background gc” w .NET 4.0. Wprowadzono kilka usprawnień, które mają za zadanie zminimalizować czas przeznaczany na blokadę wątków. W najnowszej dokumentacji można spotkać się zatem z określeniem „background”, a nie „concurrent”, który dotyczył starszych wersji. W .NET 4.0 kolekcja w tle odnosiła się wyłącznie do trybu workstation. Od wersji 4.5 dostępna jest ona również w trybie serwerowym.

Kontrola GC w trakcie wykonywania programu

Powyższe ustawienia są możliwe do zmiany wyłącznie w pliku konfiguracyjnym, a co za tym idzie, nie można ich zmieniać w trakcie wykonywania programu. Programista może również wpływać na zachowanie GC już w trakcie działania aplikacji. Klasa GCSettings eksponuje właściwość LatencyMode, która przyjmuje z kolei następujące wartości:

  • Batch,
  • LowLatency,
  • Interactive,
  • SustainedLowLatency (dodane w .NET 4.5).

Tryb LowLatency, jak sama nazwa sugeruje, przeznaczony jest dla systemów, które nie mogą sobie pozwolić na jakiekolwiek opóźnienia spowodowane odpaleniem GC. Wykonanie kolekcji GEN2 może trochę potrwać – dla pewnych, krytycznych operacji jest nie do zakceptowania, aby zostały one przerwane przez GC. Jeśli zatem wykonywana jest jakaś ważna operacja, która wymaga dużych zasobów CPU, wtedy warto pomyśleć nad chwilowym przełączeniem się w tryb LowLatency. W LowLatency, GEN0 i GEN1 będą prawdopodobnie wykonywane częściej niż zwykle, z kolei czasochłonna GEN2 będzie unikana. Oczywiście czasami może dojść do sytuacji, gdzie GEN2 zostanie wykonana, ponieważ zasoby systemowe były na wyczerpaniu. W praktyce jednak, częstotliwość wykonywania GEN2 zostanie zminimalizowania, a GEN0 oraz GEN1 mogą mieć miejsce częściej. Należy mieć na uwadze, że aplikacja w takim trybie będzie pochłaniała więcej zasobów niż w trybie domyślnym, co jest naturalnym efektem unikania GEN2. Z tego względu, przełączanie do LowLatency powinno mieć charakter krótkotrwały. Po wykonaniu krytycznej operacji, powinno się powrócić z powrotem do poprzedniego trybu.

Dobrym zwyczajem jest użycie CER, aby zmniejszyć ryzyko niepowodzenia operacji (np. spowodowane OutOfMemoryException):

GCLatencyMode previousMode = GCSettings.LatencyMode;            
System.Runtime.CompilerServices.RuntimeHelpers.PrepareConstrainedRegions();
try
{
    GCSettings.LatencyMode = GCLatencyMode.LowLatency;
    // wykonanie operacji krytycznej, ktora nie powinna byc spowolniona przez GC 
}
finally
{
    GCSettings.LatencyMode = previousMode;
}

Ponadto w LowLatency nie powinno deklarować się dużych obiektów, ponieważ będą one alokowane na GEN2, co spowoduje, że aplikacja jeszcze więcej będzie zużywać pamięci, która w normalnym trybie dawno zostałaby już zwolniona.

Tryb Batch jest z kolei całkowitym zaprzeczeniem LowLatency. Spowoduje on, że kolekcje wykonywane są jeden po drugim, bez wsparcia równoległego wykrywania obiektów nieosiągalnych. Ustawienie LatencyMode na Batch spowoduje więc wyłączenie concurrent gc. Dla aplikacji UI, jest to bardzo złe rozwiązanie, ponieważ użytkownik, mógłby się zorientować, że coś jest wykonywane w tle, a interfejs użytkownika nie zareaguje prawidłowo – tylko z opóźnieniem lub w ogóle. Gdy użytkownik samodzielnie zablokuje concurrent gc, wtedy tryb Batch ustawiany jest domyślnie. Dla aplikacji UI jest to złe rozwiązanie. Z kolei dla serwerowych może okazać się dobre – należy mieć na uwadze, że równoległy GC zajmuje więcej zasobów z tego względu, że śledzenie obiektów nieosiągalnych wykonywane jest na bieżąco.

Tryb Interactive jest domyślny dla „concurrent workstation” i przeznaczony jest dla aplikacji klienckich z UI. Obiekty nieosiągalne śledzone są na bieżąco w osobnym wątku. W przeciwieństwie do LowLatency, nie jest to tryb bardzo inwazyjny, powodujący wysokie zużycie pamięci – po prostu jest to kompromis używany dla aplikacji klienckich. Dla aplikacji serwerowych lepszym rozwiązaniem może okazać się batch, który nie zużywa tyle pamięci.

Batch zatem daje większą przepustowość (brak dodatkowego wątku, który spowalnia i wymaga dodatkowych zasobów na równoległe przeszukiwanie). Z kolei tryb Interactive gwarantuje mniejszą przepustowość, ale za to płynność wykonywania operacji jest większa. W .NET 4.5 stworzono kompromis między tymi rozwiązaniami, jakim jest SustainedLowLatency. SustainedLowLatency będzie starał się nie powodować zbyt długich przerw na GC, ale jeśli będzie to potrzebne, inne wątki zostaną zablokowane w celu wykonania kolekcji. Dzięki temu, bezpieczne jest włączanie SustainedLowLatency na dłuższy czas (w przeciwieństwie do LowLatency), ale jednocześnie można cieszyć się płynnością działania aplikacji.

Zakończenie

W przypadku wielu aplikacji można polegać na domyślnych ustawieniach, a ewentualna ich zmiana może spowodować tylko skutki uboczne. Warto mimo wszystko znać ustawienia, aby móc trafnie zdiagnozować ewentualne problemy związane z wydajnością aplikacji. Istnieje jednak pewien zbiór aplikacji, gdzie strata każdej milisekundy wpływa na efekt końcowy. W językach niezarządzanych, takich jak C++, nie istniał Garbage Collector i przez to aplikacje nie musiały zatrzymywać się w celu wykonania kolekcji – spoczywało to na programiście. W systemach czasu rzeczywistego, gdzie wymagane są jak najmniejsze opóźnienia („low latency”), trzeba poważnie wziąć pod uwagę ustawienia GC. Oczywiście zmiany mogą mieć również efekt niekorzystny, dlatego zawsze trzeba posiadać dobrze zdefiniowany zbiór testów wydajnościowych, aby móc każdą zmianę poprzeć dowodami w formie testów.

 


          

Piotr Zieliński

Absolwent informatyki o specjalizacji inżynieria oprogramowania Uniwersytetu Zielonogórskiego. Posiada szereg certyfikatów z technologii Microsoft (MCP, MCTS, MCPD). W 2011 roku wyróżniony nagrodą MVP w kategorii Visual C#. Aktualnie pracuje w General Electric pisząc oprogramowanie wykorzystywane w monitorowaniu transformatorów . Platformę .NET zna od wersji 1.1 – wcześniej wykorzystywał głównie MFC oraz C++ Builder. Interesuje się wieloma technologiami m.in. ASP.NET MVC, WPF, PRISM, WCF, WCF Data Services, WWF, Azure, Silverlight, WCF RIA Services, XNA, Entity Framework, nHibernate. Oprócz czystych technologii zajmuje się również wzorcami projektowymi, bezpieczeństwem aplikacji webowych i testowaniem oprogramowania od strony programisty. W wolnych chwilach prowadzi blog o .NET i tzw. patterns & practices.