Słowo kluczowe fixed w c#

Wielu z Was kojarzy zapewne konstrukcję unsafe do deklarowania stref niezarządzanych w których możemy np. wykorzystywać wskaźniki i inne mechanizmy znane z języków niezarządzanych. Słowo fixed wydaje mi się, że jest nieco mniej popularne.

Wiemy, że w środowisku .NET, gdzie zasoby pamięci zarządzane są przez Garbage Collector, obiekty mogą  zmieniać swój adres. W poprzednich postach (o GC) pisałem, że przy zwolnieniu obiektów, wszystkie pozostałe są szeregowane jeden po drugim, tak aby uniknąć problemów związanych z fragmentacją – nowe obiekty zawsze są doczepiane na końcu. Sam ten fakt, udowadnia, że nie możemy polegać na czystych wskaźnikach. Wyobraźmy sobie taką konstrukcję:

MyClass myClass = new MyClass();
unsafe
{
    int* pointer = &myClass.Age; //Age - pole typu int
}

Powyższy kod z pewnością nie jest poprawny dla .NET. MyClass nie ma stałego adresu, w każdym momencie może zostać przeniesiony przez GC i po pewnym czasie zmienna pointer będzie wskazywała na coś innego. Rozwiązaniem jest oczywiście słowo fixed:

MyClass myclass = new MyClass();
unsafe
{
 fixed(int* field = &myclass.Age)
 {
     
 }
}

Fixed nakazuje GC, aby adres był stały, ustalony z góry. Dzięki temu możemy w bezpieczny sposób odwoływać się do wskaźnika – mamy pewność, że nie zostanie zmieniony.

Garbage Collector, część V– destruktory, wydajność, przykład

W ostatnim poście obiecałem pokazać na przykładzie, że destruktory rzeczywiście mają negatywny wpływ na wydajność. Mamy prostą klasę:

class MyClass
{
    ~ MyClass()
    {
        // Jakis bezensowny kod np:
        for (int i = 0; i <  100; i++){}
    }
}

Następnie stwórzmy kilkaset obiektów:

Stopwatch stopwatch=new Stopwatch();
stopwatch.Start();
for (int i = 0; i < 100 * 10000; i++)
{
    var newInstance = new MyClass();
}
stopwatch.Stop();
long duration = stopwatch.ElapsedMilliseconds;

Przyjrzyjmy się teraz diagramowi przedstawiającemu rozkład obiektów względem generacji (CLR Profiler):

image

Na moim komputerze duration wyniósł 900. Usuńmy destruktor i dokonajmy analogicznej obserwacji:

image

Czas deklaracji wyniósł 450. Jakie wnioski? Oczywiście użycie destruktorów spowodowało wypromowanie zwalnianych obiektów do drugiej generacji. Proporcje GEN0 do GEN1 niemal są odwrotne – bez destruktora większość obiektów zawalnianych jest w GEN0.  Ponadto sama inicjalizacja (wykonanie pętli) zabrała 2x więcej czasu. Rozwiązaniem jest użycie interfejsu IDisposable.

Garbage Collector, część IV – destruktor i problemy

Jeśli miał ktoś do czynienia np. z CPP z pewnością kojarzy pojęcie destruktora. Jest to metoda, wywoływana w momencie zwalniania obiektu  z pamięci (przeciwieństwo konstruktora). Zarówno w CPP jak w C#,  nazwa destruktora stanowi ‘~’ plus nazwa klasy. Przykład:

class MyClass
{
    MyClass(){}// standardowy konstruktor

    ~ MyClass()
    {
           // obiekt zwalniany z pamieci
    }
}

Jednak sam sposób działania destruktorów w C# jest całkowicie inny od CPP.  Przede wszystkim destruktor to nic innego jak metoda Finalize:

protected override void Finalize()
{
   try
   {
      // ciało destruktora
   }
   finally
   {
      base.Finalize();
   }
}

Na poziomie kompilacji, generowana jest metoda Finialize (zamiast destruktora). Generalnie Finalize to wirtualna metoda typu protected, w której możemy umieścić wszelkie zwalnianie zasobów (głównie niezarządzanych). Czy użyjemy składni destruktora czy bezpośrednio przeładujemy Finalize nie ma to żadnego znaczenia.

Wszystko byłoby fajne, mamy w końcu miejsce w którym możemy zwalniać zasoby. Klasy z destruktorem\Finalize są jednak DUŻO trudniejsze do usunięcia z pamięci ponieważ:

  1. Gdy seria obiektów nie ma Finalize wtedy GC po prostu czyści pamięci od początkowego adresu aż do końca. W przypadku Finalize musi po kolei wywoływać Finalize i zwalniać pamięć – nie da się tego zrobić w jednym batch’u.
  2. Istnieje duża szansa, że obiekt z Finalize dostanie promocję do GEN1 lub nawet GEN2 (o generacjach pisałem w poprzednich postach ). Generalnie zwolnienie pamięci z GEN1 jest dużo wolniejsze niż z GEN0. A dlaczego to może się zdarzyć?

Przede wszystkim GC oprócz listy obiektów zawiera również specjalną kolejkę (finalization queue), która zawiera wskaźniki do obiektów zawierających metodę finalize. Załóżmy, że wywołana jest metoda GC.Collect. GC sprawdza, które obiekty są nieosiągalne. Załóżmy również, że obiekt zawierający Finalize jest teraz nieosiągalny. GC przeszukuje finalization queue i dodaje dany obiekt do następnej kolejki nazwanej freachable queue (finalization reacheable). Jeśli zwalniany obiekt nie znajdowałby się w finalization queue (ponieważ nie ma destruktora) wtedy po prostu standardowo zostałby zwolniony. W tym przypadku został jednak przeniesiony do freachable queue i z tego względu z powrotem jest widziany jako obiekt osiągalny – nie został w końcu jeszcze zwolniony. I to jest właśnie moment, kiedy GC może ponownie przeszukać GEN0 i awansować obiekt do następnej generacji – ponieważ obiekty w freachable queue traktowane są jako osiągalne.

Zatem domyślnie wszystkie obiekty z destruktorem (a właściwie ich wskaźniki) umieszczane są w finalization queue. Jeśli staną się one nieosiągalne wtedy przesuwamy je do freachable queue i w tym momencie znów widziane są jako osiągalne. Co dalej? Do dyspozycji jest osobny wątek, który wywołuje metodę Finalize po kolei dla wszystkich obiektów w freachable queue. Po wywołaniu Finalize, obiekt może zostać zwolniony już całkowicie z pamięci. Ale to pokazuje, ze potrzebujemy przynajmniej dwóch GC.Collect aby usunąć obiekt z pamięci: najpierw aby przenieść go do freachable queue , potem czekamy aż osobny wątek wywoła Finalize a dopiero wtedy, następna kolekcja (GC.Collect) zwolni obiekt całkowicie. Ponadto bardzo jest prawdopodobne, że w międzyczasie obiekt zostanie awansowany do następnej generacji, co spowoduje znaczący spadek wydajności.

Umieszczenie destruktora powoduje zatem dość istotny spadek wydajności. Dużo lepszą praktyką jest użycie interfejsu IDisposable o którym pisałem tutaj. W następny poście napiszemy kotiki programik, który pokaże, że faktycznie destruktory powodują problemy.

Garbage Collector, część III–healthy GC

W poprzednim poście przedstawiłem zasadę działania generacji w GC.  Dowiedzieliśmy się, że zwalnianie zasobów z generacji 0 jest bardzo szybkie z kolei z GEN 2 wolne. Healthy GC to reguła określająca optymalny (zdrowy) stan GC:

gen0 : gen1 : gen2 => 100 : 10 : 1

W Internecie można również znaleźć nieco inne wartości ale ogólna zasada jest taka sama: GEN0 powinna zawierać dużo więcej obiektów niż GEN2. Jeśli obiekty nie są zwalniane, wtedy promowane są do generacji pierwszej i drugiej. Z poprzedniego posta pamiętamy, że GC.Collect na GEN2 może być BARDZO kosztowny. Jeśli architektura aplikacji przewiduje, że w GEN2 będzie porównywalnie dużo obiektów do GEN0, wtedy mamy bardzo poważny problem wydajnościowy.

Przykład skrajnie niedobrej proporcji:

GC0: 10000, GC1: 10000, GC2: 10000

Co powyższe liczby oznaczają? Prawdopodobnie obiekty nie są po prostu zwalnianie i przy kolejnych GC.Collect zostały wypromowane do następnych generacji. Gdy GC poświęca dużo czasu na GC.Collect (np. ciągle 5%) tzn., że nie mamy Healthy GC. W Healthy GC, kolekcja jest szybka i GC nie obciąża tak bardzo CPU.

Garbage Collector–część II

W poprzednim poście przedstawiłem ogólne zasady działania GC w zarządzanych językach. Dziś przyjrzymy się bardziej na konstrukcję rozwiązania Microsoft’owego. Zakładam, że czytelnik zna już algorytm Mark&Sweep. Pamięć alokowana w .NET jest przechowywana w tzw. generacjach. Istnieją 3 generacje:

  1. Generation 0 – zwolnienie obiektu z GEN0 jest szybkie i mało kosztowne. Przechowywane są w niej obiekty używane tylko przez krótki czas.
  2. Generation 1  – obiekty, które awansowały z GEN0. Zwolnienie zasobów w GEN1 jest wolniejsze.
  3. Generation 2 – analogicznie do GEN1 – usuwanie zasobów jest wolniejsze niż ma to miejsce w GEN0 lub GEN1.

.NET zatem, ze względów wydajnościowych, przydziela zasoby do różnych generacji. Jeśli obiekt jest używany przez krótki czas a potem zwalniany, ma to wtedy miejsce w GEN0. Jeśli podczas zwalniania zasobów, czyli w momencie wywołania metody GC.Collect(), któreś z obiektów przetrwają wtedy awansowane są do następnej generacji.  GC analizuje ile razy obiekt musi przetrwać wywołanie GC.Collect aby zostać awansowany do następnej generacji. Wartości tych progów przydziale są przez GC dynamicznie tak aby było to maksymalnie optymalne.

Jeśli np. obiekt nie został zwolniony podczas 10 GC.Collect (Mark&Sweep), wtedy bardzo prawdopodobne jest, że przez większość czasu działania aplikacji będzie on potrzebny – a co za tym idzie szkoda marnować czasu na mieszanie go z obiektami krótkotrwałymi. Ktoś może zadać pytanie, ale co za problem trzymać wszystkie zasoby w jednej generacji. GC po każdym zwalnianiu pamięci porządkuje zasoby tak, że nie ma pomiędzy obiektem A a obiektem B wolnej przestrzeni. Jeśli obiektA zużywa zasoby pomiędzy adresem 0-4, to obiektB będzie zajmował od 5-9. Dzięki temu, deklaracja nowych obiektów jest szybka ponieważ bardzo łatwo wyznaczyć adres w pamięci – jest to po prostu następna liczba (w naszym przypadku obiekt3 byłby zadeklarowany w polu 10). Unikamy również problemu fragmentacji. Obiekty długotrwałe powodowałyby problem ponieważ należałoby je “przesuwać” po każdym GC.Collect oraz po co wykonywać algorytm Mark&Sweep dla drzewa obiektów, które jest potrzebne przez 90% czasu działania aplikacji?

Podsumowując:

  1. W celu optymalizacji wprowadzono 3 generacje. GEN0 jest szybka i zawiera obiekty często deklarowane i zwalniane.
  2. Pamięć jest deklarowana w sposób zwarty – nie ma wolnej przestrzeni między obiektami (brak fragmentacji).
  3. Obiekty w tej samej generacji są mniej więcej używane w aplikacji przez taki sam czas.
  4. Tylko część pamięci jest przeznaczona dla aplikacji .NET (tzw. commited memory space). Jeśli potrzeba więcej wtedy system operacyjny z “uncommited space” przydziela pamięć do “commited space”.
  5. GC.Collect uruchamiany jest gdy pewien próg zużycia pamięci zostanie przekroczony – a nie natychmiast gdy któryś z obiektów jest już niepotrzebny.

Posiadając już wiedzę o generacjach, w następny poście wyjaśnię co to jest tzw. Healthy GC.

Artykuł: Orchard CMS – Integracja z Windows Azure

Orchard jest potężnym systemem CMS, wyróżniającym się elastycznością i rozszerzalnością. Autorzy frameworku pomyśleli również o łatwej integracji z platformą Azure. Wystarczy stworzyć odpowiednią paczkę (wykorzystując narzędzia dostarczone przez autorów Orchard), a następnie umieścić ją w Azure.

http://msdn.microsoft.com/pl-pl/library/orchard-cms–integracja-z-windows-azure.aspx

Garbage Collector – część I (algorytm mark and sweep).

Aby pisać kod, który jest wydajny i optymalny należy dobrze zrozumieć jak działa Garbage Collector (GC). W dzisiejszym poście przedstawię ogólne zasady działania GC na przykładzie algorytmu mark and sweep. Zaznaczam, że implementacja w .NET różni się i jest dużo bardziej wyrafinowana – ale o tym w następnych postach. Chcę najpierw przedstawić algorytm mark and sweep ponieważ da to czytelnikowi ogólny obraz zagadnienia związanego ze zwalnianiem pamięci w językach zarządzanych.

Garbage Collector oczywiście służy do zwalniania pamięci i wyręcza nas z pamiętania jakie zasoby już nie są potrzebne. Oczywiście czasami zachodzi potrzeba ręcznego zwolnienia pamięci ale o tym w przyszłych postach. Wielu z nas pewnie pamięta operator delete z CPP czy free z C. W językach zarządzanych wytropieniem niepotrzebnych obiektów zajmuję się Garbage Collector.

Podstawowy algorytm mark and sweep sprowadza się do dwóch faz: mark (znakowanie) oraz sweep (usuwanie). Najpierw (faza mark), począwszy od korzeni, wszystkie obiekty są znakowane jakąś flagą. Potem w fazie sweep, wszystkie NIE oznaczone obiekty są usuwane. Faza MARK służy zatem do znalezienia potrzebnych, używanych obiektów. Skoro można dotrzeć do jakiegoś obiektu od korzenia, to znaczy, że jest on używany (seria referencji). Korzeniem mogą być na przykład zmienne globalne.

Algorytm w pseudokodzie można naszkicować następująco:

void Collect()
{
    // Faza I
    for each root (r)
        Mark (r);
    // Faza II
    Sweep ();
}

Metoda Mark jest oczywiście rekurencyjna – jako parametr przyjmuje wskaźnik do korzenia a następnie przeszukuje całe drzewo obiektów. Spróbujmy naszkicować metodę Mark:

void Mark (object objectPointer)
{
    if (objectPointer.IsMarked == false)
    {
        objectPointer.IsMarked = true; 
        
        for each Object child of objectPointer)
        {
            Mark(child);
        }
    }
}

Należy sprawdzać czy obiekt jest już oznakowany aby uniknąć zapętleń. Przyjrzymy się drugiej fazie (sweep):

void Sweep ()
{
    foreach objectPointer in the heap
    {
        if (objectPointer.IsMarked) 
        {
            objectPointer.IsMarked = false
        }
        else
        {
            Release(objectPointer);
        }
    }    
}

Proszę zwrócić uwagę, że ustawiamy flagę na false – dzięki temu przy następnym uruchomieniu GC, będzie możliwa pierwsza faza. Wszystkie obiekty nieoznakowane są usuwane z pamięci.

Oczywiście GC nie zwalnia programisty z myślenia Uśmiech. Bardzo łatwo popełnić błąd i spowodować Memory Leak – o tym w ostatnim poście.

Code Review: pola w C#

Co byście powiedzieli na taki kod?

public class Person
{
   public string FirstName;
   public string  LastName;

   public string GetFullName()
   {
       return string.Format("{0} {1}", FirstName, LastName);
   }       
}

Na pierwszy rzut oka może nic poważnego. Jednak jeśli chcemy pisać kod zgodny z praktykami C#, powinniśmy zwrócić uwagę na następujące kwestie:

  • Enkapsulacja, FirstName,LastName powinni być ukryte i ewentualnie wyeksponowane za pomocą setterow\getterów.
  • GetFullName jest dobrym sposobem dla Javy (a w zasadzie getFullName). W C# do tego celu używamy właściwości.

Po refaktoryzacji otrzymujemy:

public class Person
{
    private string _firstName;
    public string FirstName
    {
        get{return _firstName;}
        set{ _firstName = value;}
    }
    private string _lastName;
    public string LastName
    {
        get{return _lastName;}
        set{ _lastName = value;}
    }

    public string FullName
    {
        get{ return string.Format("{0} {1}",_firstName, _lastName);}
    }
}

Kod wygląda dużo lepiej. Osobiście jednak dokonałbym dwóch modyfikacji:

  1. Jeśli nie mamy żadnej logiki (typu wywołanie zdarzenia) w setterach, lepiej użyć automatic properties.
  2. “Owinąć” w regiony kod.

Po kolejnej refaktoryzacji otrzymujemy:

public class Person
{
   #region Properties

   public string FirstName { get; set; }
   public string LastName { get; set; }
   public string FullName
   {
       get { return string.Format("{0} {1}", FirstName, LastName); }
   }

   #endregion
}