Dlaczego sterta dużych obiektów i dlaczego nas to obchodzi?

105

Czytałem o pokoleniach i stosie dużych obiektów. Ale nadal nie rozumiem, jakie jest znaczenie (lub korzyść) posiadania dużej sterty obiektów?

Co mogłoby pójść nie tak (pod względem wydajności lub pamięci), gdyby środowisko CLR po prostu polegało na generacji 2 (biorąc pod uwagę, że próg dla Gen0 i Gen1 jest mały do ​​obsługi dużych obiektów) do przechowywania dużych obiektów?

Manish Basantani
źródło
6
To daje mi dwa pytania do projektantów .NET: 1. Dlaczego defragmentacja LOH nie jest wywoływana przed wyrzuceniem wyjątku OutOfMemoryException? 2. Dlaczego nie obiekty LOH mają powinowactwo do bycia razem (duży wolą koniec hałdy i mała na początku)
Jacob Brewer

Odpowiedzi:

195

Wyrzucanie elementów bezużytecznych nie tylko usuwa obiekty, do których nie istnieją odniesienia, ale także kompaktuje stertę. To bardzo ważna optymalizacja. Nie tylko zwiększa to wykorzystanie pamięci (brak nieużywanych dziur), ale także znacznie zwiększa wydajność pamięci podręcznej procesora. Pamięć podręczna to naprawdę poważna sprawa w przypadku nowoczesnych procesorów, są one o rząd wielkości szybsze niż szyna pamięci.

Kompaktowanie odbywa się po prostu przez kopiowanie bajtów. To jednak wymaga czasu. Im większy obiekt, tym większe prawdopodobieństwo, że koszt jego skopiowania przeważa nad możliwą poprawą wykorzystania pamięci podręcznej procesora.

Przeprowadzili więc szereg testów porównawczych, aby określić próg rentowności. I osiągnął 85 000 bajtów jako punkt odcięcia, w którym kopiowanie nie poprawia już wydajności. Ze specjalnym wyjątkiem dla tablic double, są one uważane za „duże”, gdy tablica ma więcej niż 1000 elementów. To kolejna optymalizacja dla 32-bitowego kodu, alokator dużych stert obiektów ma specjalną właściwość polegającą na tym, że przydziela pamięć pod adresy, które są wyrównane do 8, w przeciwieństwie do zwykłego alokatora pokoleń, który przydziela tylko wyrównane do 4. Takie wyrównanie jest wielką sprawą dla podwójnych , czytanie lub pisanie źle wyrównanego podwójnego jest bardzo kosztowne. Co dziwne, rzadkie informacje firmy Microsoft nigdy nie wspominają o tablicach długich, nie jestem pewien, o co chodzi.

Fwiw, jest wiele obaw programistów co do tego, że sterta dużych obiektów nie jest kompaktowana. To niezmiennie jest wyzwalane, gdy piszą programy, które zajmują ponad połowę całej dostępnej przestrzeni adresowej. Następnie za pomocą narzędzia takiego jak profiler pamięci, aby dowiedzieć się, dlaczego program zbombardował, mimo że wciąż było dostępnych dużo nieużywanej pamięci wirtualnej. Takie narzędzie pokazuje dziury w LOH, nieużywane fragmenty pamięci, w których wcześniej mieszkał duży obiekt, ale zbierano śmieci. Taka jest nieunikniona cena LOH, dziura może być ponownie wykorzystana tylko przez alokację dla obiektu o równym lub mniejszym rozmiarze. Prawdziwy problem polega na założeniu, że program powinien mieć możliwość wykorzystania całej pamięci wirtualnej w dowolnym momencie.

Problem, który w przeciwnym razie znika całkowicie po uruchomieniu kodu w 64-bitowym systemie operacyjnym. Proces 64-bitowy ma 8 terabajtów dostępnej przestrzeni adresowej pamięci wirtualnej, o 3 rzędy wielkości więcej niż proces 32-bitowy. Po prostu nie możesz zabraknąć dziur.

Krótko mówiąc, LOH sprawia, że ​​kod działa wydajniej. Kosztem mniej wydajnego wykorzystania dostępnej przestrzeni adresowej pamięci wirtualnej.


UPDATE, .NET 4.5.1 obsługuje teraz kompaktowanie właściwości LOH, GCSettings.LargeObjectHeapCompactionMode . Uważaj na konsekwencje, proszę.

Hans Passant
źródło
3
@Hans Passant, czy mógłbyś wyjaśnić kwestię systemu x64, masz na myśli, że ten problem całkowicie zniknął?
Johnny_D
Niektóre szczegóły implementacji LOH mają sens, ale niektóre mnie zastanawiają. Na przykład, mogę zrozumieć, że jeśli tworzy się i porzuca wiele dużych obiektów, ogólnie może być pożądane usuwanie ich masowo w kolekcji Gen2 niż fragmentarycznie w kolekcjach Gen0, ale jeśli tworzy się i porzuca np. Tablicę 22000 ciągów, do których nie istnieją żadne zewnętrzne odniesienia, jakie korzyści płyną z tego, że kolekcje Gen0 i Gen1 oznaczają wszystkie 22 000 ciągów jako „na żywo” bez względu na to, czy istnieje odniesienie do tablicy?
supercat
6
Oczywiście problem fragmentacji jest taki sam na x64. Będzie tylko wziąć kilka dni więcej uruchamiając proces serwera przed rzutach.
Lothar
1
Hmm, nie, nigdy nie lekceważ 3 rzędów wielkości. To, ile czasu zajmuje zebranie śmieci ze sterty 4 terabajtów, jest czymś, czego nie można uniknąć na długo, zanim się do tego zbliży.
Hans Passant
2
@HansPassant Czy mógłbyś, proszę, rozwinąć to stwierdzenie: „Ile czasu zajmuje zebranie śmieci ze sterty 4 terabajtów to coś, czego nie można uniknąć na długo, zanim się do tego zbliży”.
stosunkowo_random
9

Jeśli rozmiar obiektu jest większy niż pewna przypięta wartość (85000 bajtów w .NET 1), środowisko CLR umieszcza go w stosie dużych obiektów. To optymalizuje:

  1. Alokacja obiektów (małe obiekty nie są mieszane z dużymi obiektami)
  2. Wyrzucanie elementów bezużytecznych (LOH zbierane tylko na pełnym GC)
  3. Defragmentacja pamięci (LOH nigdy nie jest rzadko kompaktowany)
oleksii
źródło
9

Zasadnicza różnica między stertą małych obiektów (SOH) i stertą dużych obiektów (LOH) polega na tym, że pamięć w SOH jest kompaktowana po zebraniu, a LOH nie, jak ilustruje ten artykuł . Kompaktowanie dużych obiektów kosztuje dużo. Podobnie jak w przykładach w artykule, powiedzmy, że przeniesienie bajtu w pamięci wymaga 2 cykli, a następnie kompaktowanie obiektu o wielkości 8 MB w komputerze 2 GHz wymaga 8 ms, co jest dużym kosztem. Biorąc pod uwagę, że duże obiekty (w większości przypadków tablice) są w praktyce dość powszechne, przypuszczam, że to jest powód, dla którego Microsoft przypina duże obiekty do pamięci i proponuje LOH.

BTW, zgodnie z tym postem , LOH zwykle nie generuje problemów z fragmentami pamięci.

winogrono
źródło
1
Ładowanie dużych ilości danych do zarządzanych obiektów zwykle przyćmiewa 8 ms koszt kompaktowania LOH. W praktyce w większości aplikacji związanych z dużymi zbiorami danych koszt LOH jest niewielki w porównaniu z pozostałą wydajnością aplikacji.
Shiv
3

Zasadą jest to, że jest mało prawdopodobne (i całkiem możliwe, że zły projekt), aby proces utworzył wiele dużych obiektów o krótkim czasie życia, więc środowisko CLR przydziela duże obiekty do oddzielnej sterty, na której uruchamia GC według innego harmonogramu niż zwykły stos. http://msdn.microsoft.com/en-us/magazine/cc534993.aspx

Myles McDonnell
źródło
Również umieszczanie dużych obiektów, powiedzmy, 2.generacji może skończyć się pogorszeniem wydajności, ponieważ kompaktowanie pamięci zajęłoby dużo czasu, zwłaszcza jeśli uwolniono niewielką ilość, a OGROMNE obiekty musiały zostać skopiowane w nowe miejsce. Bieżący LOH nie jest zagęszczany ze względu na wydajność.
Christopher Currens,
Myślę, że to tylko zły projekt, ponieważ GC nie radzi sobie z nim dobrze.
CodesInChaos
@CodeInChaos Najwyraźniej są pewne ulepszenia nadchodzące w .NET 4.5
Christian.K
1
@CodeInChaos: Chociaż sensowne może być, aby system czekał na kolekcję gen2, zanim spróbuje odzyskać pamięć nawet z krótkotrwałych obiektów LOH, nie widzę żadnej korzyści w wydajności w deklarowaniu obiektów LOH (i wszelkich obiektów, które przechowują odniesienia) bezwarunkowo żyć podczas kolekcji gen0 i gen1. Czy są jakieś optymalizacje, które są możliwe dzięki takiemu założeniu?
supercat
@supercat Spojrzałem na link, o którym wspomniał Myles McDonnell. Rozumiem: 1. Zbieranie LOH odbywa się w GC gen 2. 2. Kolekcja LOH nie obejmuje zagęszczania (do czasu pisania artykułu). Zamiast tego oznaczy martwe obiekty jako wielokrotnego użytku, a te dziury będą służyć przyszłym alokacjom LOH, jeśli będą wystarczająco duże. Ze względu na punkt 1, biorąc pod uwagę, że gen 2 GC byłby powolny, gdyby było wiele obiektów w gen 2, myślę, że lepiej jest unikać używania LOH w jak największym stopniu w tym przypadku.
robbie fan
0

Nie jestem ekspertem od CLR, ale wyobrażam sobie, że posiadanie dedykowanej sterty dla dużych obiektów może zapobiec niepotrzebnym zamiataniu GC istniejących hałd pokoleniowych. Przydzielanie dużego obiektu wymaga znacznej ilości ciągłej wolnej pamięci. Aby to zapewnić z rozproszonych "dziur" w pryzmach pokoleń, należałoby często zagęszczać (które są wykonywane tylko w cyklach GC).

Chris Shain
źródło