W dzisiejszym wpisie, pokażę jaki wpływ mają klasy na zużycie pamięci. W ostatnim poście pokazałem korzyści płynące ze struktur jeśli mamy do czynienia z małymi kontenerami na dane. Najlepiej odpalmy po prostu następujący kod:
internal class Test { const int n = 10000000; private static void Main(string[] args) { TestStruct(); TestClass(); } private static void TestStruct() { long current = GC.GetTotalMemory(true); var values = new PointValue[n]; long after = GC.GetTotalMemory(true); Console.WriteLine("Struktury, potrzebna pamiec: {0}", after - current); } private static void TestClass() { long current = GC.GetTotalMemory(true); var values = new PointRef[n]; for (int i = 0; i < values.Count(); i++) values[i] = new PointRef(); long after = GC.GetTotalMemory(true); Console.WriteLine("Klasy, potrzebna pamiec: {0}",after - current); } }
W programiku po prostu tworzymy i alokujemy tablicę struktur oraz klas. Wyniki są następujące:
Liczby tak naprawdę nie powinni zadziwiać – klasy tworzą masę dodatkowych pól co dla prostych typów nie jest przecież wykorzystywane. Oprócz tego warto zwrócić uwagę na kilka innych aspektów. Sama alokacja tablicy klas jest dość powolnym procesem ponieważ należy przejść przez taką tablicę i stworzyć każdy element osobno. W przypadku struktur jest to bardzo szybkie ponieważ wszystkie elementy tworzone są automatycznie od razu – stąd nie można definiować ręcznie domyślnych konstruktorów w strukturach.
Inną kwestią jest zwolnienie zasobów co jest naprawdę ogromną wadą klas. Proszę zauważyć, że GC ma dużo do roboty, gdy mamy wiele małych obiektów. W takiej sytuacji musimy przejść poprzez graf składający się z wielu węzłów. Im mniej obiektów, tym mniej poszukiwań i tym szybsza kolekcja.
Warto również zrobić analogiczny eksperyment, pokazujący czas alokacji oraz zwolnienia pamięci:
internal class Test { const int n = 10000000; private static void Main(string[] args) { Stopwatch stopwatch = Stopwatch.StartNew(); TestStruct(); GC.Collect(); GC.WaitForPendingFinalizers(); Console.WriteLine("Alokacja oraz zwolnienie struktur: {0}",stopwatch.ElapsedTicks); stopwatch = Stopwatch.StartNew(); TestClass(); GC.Collect(); GC.WaitForPendingFinalizers(); Console.WriteLine("Alokacja oraz zwolnienie klas: {0}", stopwatch.ElapsedTicks); } [MethodImpl(MethodImplOptions.NoInlining)] private static void TestStruct() { var values = new PointValue[n]; } [MethodImpl(MethodImplOptions.NoInlining)] private static void TestClass() { var values = new PointRef[n]; for (int i = 0; i < values.Count(); i++) values[i] = new PointRef(); } }
Wynik:
Różnica jest znów ogromna, z powodów opisanych w ostatnich wpisach. Tablica struktur to jedna ciągła całość w pamięci, z kolei w przypadku klas są to wskaźniki do obiektów. Czas wykonania kolekcji zależy od liczby obiektów, przede wszystkim tych osiągalnych. Pamiętajmy, że algorytm Mark&Sweep musi najpierw zaznaczyć wszystkie osiągalne obiekty aby potem usunąć te nieosiągalne (niezaznaczone). Z tego wniosek, że szybciej jest przejść przez taki graf, gdzie większość obiektów jest nieosiągalna albo są to duże obiekty, których nie jest tak wiele w systemie.