Kiedy warto wymusić zbieranie śmieci?


135

Czytałem więc pytanie o zmuszeniu modułu śmieciowego C # do uruchomienia tam, gdzie prawie każda odpowiedź jest taka sama: możesz to zrobić, ale nie powinieneś - z wyjątkiem bardzo rzadkich przypadków . Niestety nikt nie wyjaśnia, jakie są takie przypadki.

Czy możesz mi powiedzieć w jakim scenariuszu wymuszenie zbierania śmieci jest dobrym lub rozsądnym pomysłem?

Nie pytam o przypadki specyficzne dla C #, ale raczej wszystkie języki programowania, które mają moduł wyrzucania elementów bezużytecznych. Wiem, że nie możesz wymuszać GC na wszystkie języki, takie jak Java, ale załóżmy, że możesz.


17
„ale raczej wszystkie języki programowania, które mają moduł wyrzucania elementów bezużytecznych” Różne języki (lub, właściwie, różne implementacje ) używają różnych metod do wyrzucania elementów bezużytecznych, więc jest mało prawdopodobne, aby znaleźć regułę uniwersalną.
Pułkownik Trzydzieści Dwa

4
@Doval Jeśli jesteś ograniczony w czasie rzeczywistym, a GC nie zapewnia pasujących gwarancji, jesteś pomiędzy kamieniem a trudnym miejscem. Może to zmniejszyć niepożądane przerwy w porównaniu do nie robienia niczego, ale z tego, co słyszałem, „łatwiej” jest uniknąć przydzielania w normalnym trybie pracy.

3
Miałem wrażenie, że jeśli spodziewałeś się trudnych terminów w czasie rzeczywistym, nigdy nie używałbyś języka GC.
GregRos

4
Nie widzę, jak możesz odpowiedzieć na to pytanie w sposób nieokreślony dla maszyny wirtualnej. Odpowiedni dla procesów 32-bitowych, bez znaczenia dla procesów 64-bitowych. .NET JVM, a dla wyższej klasy
rwong

3
@DavidConrad możesz wymusić w C #. Stąd pytanie.
Omega

Odpowiedzi:


127

Naprawdę nie można wydawać ogólnych oświadczeń o odpowiednim sposobie korzystania ze wszystkich implementacji GC. Różnią się bardzo. Więc porozmawiam z .NET, o którym pierwotnie mówiłeś.

Musisz dość dokładnie znać zachowanie GC, aby zrobić to z dowolnej logiki lub powodu.

Jedyną radą dotyczącą zbiórki, jaką mogę udzielić, jest: Nigdy tego nie rób.

Jeśli naprawdę znasz skomplikowane szczegóły GC, nie będziesz potrzebować mojej rady, więc to nie będzie miało znaczenia. Jeśli jeszcze nie wiesz ze 100% pewnością, to pomoże i będziesz musiał szukać w Internecie i znaleźć odpowiedź w następujący sposób: Nie powinieneś dzwonić do GC.Collect , lub alternatywnie: Powinieneś dowiedzieć się więcej o tym, jak działa GC wewnątrz i na zewnątrz, i dopiero wtedy poznasz odpowiedź .

Jest jedno bezpieczne miejsce, w którym warto używać GC .

GC.Collect to dostępny interfejs API, którego można używać do profilowania czasów rzeczy. Możesz profilować jeden algorytm, zbierać i profilować inny algorytm od razu, wiedząc, że GC pierwszego algo nie wystąpił podczas drugiego, wypaczając wyniki.

Tego rodzaju profilowanie to jedyny raz, który sugerowałbym każdemu ręcznie.


W każdym razie wymyślony przykład

Jednym z możliwych przypadków użycia jest to, że jeśli załadujesz naprawdę duże rzeczy, trafią one do Dużych Stert Obiektów, które trafią prosto do Gen 2, chociaż znowu Gen 2 jest dla obiektów o długiej żywotności, ponieważ gromadzi się rzadziej. Jeśli wiesz, że z jakiegoś powodu ładujesz krótkotrwałe obiekty do Gen 2, możesz je szybciej usunąć, aby zmniejszyć Gen 2 i szybciej zbierać kolekcje.

To najlepszy przykład, jaki mogłem wymyślić, i to nie jest dobre - presja LOH, którą tutaj budujesz, spowodowałaby częstsze kolekcje, a kolekcje są tak częste, jak to jest - są szanse, że wyczyści LOH tak samo jak tak szybko, jak zdmuchnąłeś go tymczasowymi przedmiotami. Po prostu nie ufam sobie, że zakładam lepszą częstotliwość zbierania niż sam GC - dostrojony przez ludzi o wiele mądrzejszych niż ja.


Porozmawiajmy więc o niektórych semantykach i mechanizmach w .NET GC ... lub ...

Wszystko, co myślę, że wiem o .NET GC

Każdy, kto znajdzie tutaj błędy - popraw mnie. Znaczna część GC jest znana jako czarna magia i chociaż próbowałem pominąć szczegóły, których nie byłem pewien, prawdopodobnie nadal coś popełniłem źle.

Poniżej celowo brakuje wielu szczegółów, których nie jestem pewien, a także znacznie większej ilości informacji, których po prostu nie jestem świadomy. Wykorzystaj te informacje na własne ryzyko.


Koncepcje GC

.NET GC występuje w niespójnych czasach, dlatego nazywa się go „niedeterministycznym”, co oznacza, że ​​nie można polegać na nim w określonych momentach. Jest to również generator śmieci, co oznacza, że ​​dzieli twoje obiekty na liczbę przejść GC, przez które przeszli.

Obiekty w stercie Gen 0 przeżyły 0 kolekcji, zostały one nowo utworzone, więc ostatnio nie wystąpiła żadna kolekcja od ich wystąpienia. Obiekty na stosie Gen 1 przeszły przez jeden przebieg kolekcji, podobnie obiekty na stosie Gen 2 przeżyły 2 przebiegi kolekcji.

Teraz warto zauważyć, dlaczego odpowiednio kwalifikuje te konkretne generacje i partycje. .NET GC rozpoznaje tylko te trzy generacje, ponieważ przebiegi kolekcji, które przechodzą przez te trzy stosy, są nieco inne. Niektóre obiekty mogą przetrwać zbiór przechodzi tysiące razy. GC pozostawia je po drugiej stronie partycji sterty Gen 2, nie ma sensu ich dalej dzielić, ponieważ tak naprawdę są Gen 44; przekazywanie kolekcji jest takie samo jak wszystko na stosie Gen 2.

Te pokolenia mają cele semantyczne, a także zaimplementowane mechanizmy, które je honorują, a do nich przejdę za chwilę.


Co jest w kolekcji

Podstawowa koncepcja przekazania kolekcji GC polega na tym, że sprawdza on każdy obiekt w przestrzeni sterty, aby sprawdzić, czy nadal istnieją aktywne odwołania (korzenie GC) do tych obiektów. Jeśli dla obiektu zostanie znaleziony katalog główny GC, oznacza to, że aktualnie wykonywany kod może nadal dotrzeć do tego obiektu i go użyć, więc nie można go usunąć. Jeśli jednak nie znaleziono katalogu głównego GC dla obiektu, oznacza to, że uruchomiony proces nie potrzebuje już obiektu, więc może go usunąć, aby zwolnić pamięć dla nowych obiektów.

Teraz, kiedy skończy sprzątać kilka obiektów i pozostawi niektóre z nich w spokoju, nastąpi niefortunny efekt uboczny: wolne przestrzenie między żywymi obiektami, w których martwe zostały usunięte. Ta fragmentacja pamięci, jeśli pozostawiona sama, po prostu zmarnowałaby pamięć, więc kolekcje zwykle wykonują tak zwane „zagęszczenie”, w którym zabiorą wszystkie żywe obiekty pozostawione i ściśną je razem na stosie, dzięki czemu wolna pamięć będzie sąsiadować po jednej stronie stosu dla Gen 0.

Biorąc teraz pod uwagę 3 stosy pamięci podzielone według liczby przejść, przez które przeszły, porozmawiajmy o tym, dlaczego takie partycje istnieją.


Kolekcja Gen 0

Gen 0, będąc absolutnie najnowszymi obiektami, zwykle jest bardzo mały - więc możesz go bezpiecznie zbierać bardzo często . Częstotliwość zapewnia, że ​​sterty pozostają małe, a zbiory są bardzo szybkie, ponieważ gromadzą się na tak małej sterty. Opiera się to mniej więcej na heurystyce, która twierdzi: Znaczna większość tworzonych obiektów tymczasowych jest bardzo tymczasowa, więc tymczasowe nie będą już używane ani odwoływać się do nich prawie natychmiast po użyciu, a zatem mogą być gromadzone.


Kolekcja 1. generacji

Gen 1, będący obiektami, które nie należą do tej bardzo tymczasowej kategorii obiektów, może być raczej krótkotrwały, ponieważ nadal - ogromna część stworzonych obiektów nie jest używana długo. Dlatego Gen 1 zbiera również dość często, ponownie utrzymując niewielki stos, dzięki czemu zbiory są szybkie. Jednak zakłada się, że mniej obiektów jest tymczasowych niż Gen 0, więc zbiera się rzadziej niż Gen 0

Powiem szczerze, że nie znam mechanizmów technicznych, które różnią się między przepustką kolekcjonerską Gen 0 a Gen 1, jeśli są jakieś inne niż częstotliwość, którą zbierają.


Kolekcja Gen 2

Gen 2 teraz musi być matką wszystkich kup, prawda? Cóż, tak, mniej więcej tak. To miejsce, w którym żyją wszystkie twoje stałe obiekty - na przykład obiekt, w którym żyjesz Main(), i wszystko, do czego się Main()odwołuje, ponieważ będą one zakorzenione aż do Main()powrotu po zakończeniu procesu.

Biorąc pod uwagę, że Gen 2 jest zbiorem praktycznie wszystkiego, czego inne pokolenia nie mogły zebrać, jego obiekty są w dużej mierze trwałe lub przynajmniej żyją przynajmniej. Tak więc rozpoznanie bardzo niewielkiej części tego, co znajduje się w Gen 2, będzie w rzeczywistości czymś, co można zebrać, nie trzeba go często zbierać. Pozwala to również na spowolnienie gromadzenia, ponieważ wykonuje się o wiele rzadziej. Właśnie w tym miejscu zajęli się wszystkimi dodatkowymi zachowaniami dla nieparzystych scenariuszy, ponieważ mają czas na ich wykonanie.


Kupa dużych obiektów

Jednym z przykładów dodatkowych zachowań Gen 2 jest to, że wykonuje również kolekcję na stosie dużych obiektów. Do tej pory mówiłem całkowicie o stosie małych obiektów, ale środowisko wykonawcze .NET przydziela rzeczy o określonych rozmiarach do osobnej sterty z powodu tego, co nazwałem powyżej kompaktowaniem. Zagęszczanie wymaga przemieszczania obiektów po zakończeniu zbierania na stosie małych obiektów. Jeśli w Gen 1 znajduje się żywy obiekt o wielkości 10 MB, ukończenie zagęszczania po kolekcji zajmie znacznie więcej czasu, co spowolni kolekcję Gen 1. Tak więc obiekt 10 MB jest przydzielany do sterty dużych obiektów i zbierany podczas Gen 2, który tak rzadko się uruchamia.


Finalizacja

Innym przykładem są obiekty z finalizatorami. Umieszczasz finalizator na obiekcie, który odwołuje się do zasobów poza zakresem .NETs GC (zasoby niezarządzane). Finalizator to jedyny sposób, w jaki GC może zażądać gromadzenia niezarządzanego zasobu - zaimplementujesz go w celu ręcznego gromadzenia / usuwania / uwalniania niezarządzanego zasobu, aby upewnić się, że nie wycieknie on z twojego procesu. Kiedy GC może uruchomić finalizator obiektów, twoja implementacja wyczyści niezarządzany zasób, dzięki czemu GC będzie w stanie usunąć twój obiekt bez ryzyka wycieku zasobów.

Mechanizmem, dzięki któremu finalizatory to robią, jest bezpośrednie odwoływanie się do nich w kolejce finalizacji. Gdy środowisko wykonawcze przydziela obiekt za pomocą finalizatora, dodaje wskaźnik do tego obiektu do kolejki finalizacji i blokuje obiekt na miejscu (zwany pinowaniem), aby zagęszczenie go nie przesunęło, co spowodowałoby uszkodzenie odniesienia do kolejki finalizacji. W miarę upływu kolekcji, w końcu okaże się, że Twój obiekt nie ma już katalogu głównego GC, ale finalizacja musi zostać wykonana, zanim będzie można go zebrać. Kiedy obiekt jest martwy, kolekcja przeniesie swoje odwołanie z kolejki finalizacji i umieści odniesienie do niego w tak zwanej kolejce „FReachable”. Następnie kolekcja jest kontynuowana. W innym „niedeterministycznym” czasie w przyszłości osobny wątek znany jako wątek finalizatora przejdzie przez kolejkę FReachable, wykonując finalizatory dla każdego z wymienionych obiektów. Po zakończeniu kolejka FReachable jest pusta i odwróciła nieco w nagłówku każdego obiektu, co oznacza, że ​​nie wymaga on finalizacji (ten bit można również odwrócić ręcznie za pomocąGC.SuppressFinalizeco jest powszechne w Dispose()metodach), podejrzewam również, że odpiął obiekty, ale nie cytuj mnie w tym. Następna kolekcja, która pojawi się na stosie, w którym znajduje się ten obiekt, w końcu go zbierze. Kolekcje Gen 0 nawet nie zwracają uwagi na obiekty z tym bitem wymaganym do finalizacji, automatycznie je promuje, nawet nie sprawdzając ich rootowania. Nieukroszony obiekt, który wymaga finalizacji w Gen 1, zostanie rzucony do FReachablekolejki, ale kolekcja nie robi z nim nic innego, więc żyje w Gen 2. W ten sposób wszystkie obiekty, które mają finalizator, i nie GC.SuppressFinalizezostaną zebrane w Gen 2.


4
@FlorianMargaine tak ... mówienie czegokolwiek o „GC” we wszystkich implementacjach naprawdę nie ma sensu ..
Jimmy Hoffa 18'15

10
tl; dr: Zamiast tego użyj pul obiektów.
Robert Harvey

5
tl; dr: Do pomiaru czasu / profilowania może być przydatny.
kutschkem

3
@Den, po przeczytaniu mojego opisu powyższej mechaniki (jak je rozumiem), jaka byłaby korzyść z tego, jak ją widzisz? Czyścisz dużą liczbę obiektów - w SOH (lub LOH?)? Czy właśnie spowodowałeś wstrzymanie innych wątków dla tej kolekcji? Czy ta kolekcja właśnie promowała dwa razy więcej przedmiotów do Gen 2, niż została oczyszczona? Czy kolekcja spowodowała zagęszczenie LOH (czy masz to włączone?)? Ile masz stosów GC i czy Twój GC działa w trybie serwera lub komputera? GC to piekielna lodowa góra, zdrada jest poniżej wód. Po prostu omijaj. Nie jestem wystarczająco bystry, by wygodnie zbierać.
Jimmy Hoffa

4
Pule obiektów @RobertHarvey również nie są srebrną kulą. Generator 0 generatora śmieci jest już efektywnie pulą obiektów - zwykle ma rozmiar, aby zmieścił się na najmniejszym poziomie pamięci podręcznej, dlatego nowe obiekty są generowane w pamięci, która już znajduje się w pamięci podręcznej. Twoja pula obiektów konkuruje teraz z przedszkolem GC o pamięć podręczną, a jeśli suma przedszkola GC i twojej puli jest większa niż pamięć podręczna, to oczywiście będziesz mieć spudłowanie pamięci podręcznej. A jeśli planujesz korzystać z równoległości, musisz ponownie wdrożyć synchronizację i martwić się fałszywym udostępnianiem.
Doval,

68

Niestety nikt nie wyjaśnia, jakie są takie przypadki.

Dam kilka przykładów. Podsumowując, rzadko zdarza się, że wymuszenie GC jest dobrym pomysłem, ale może być całkowicie tego warte. Ta odpowiedź pochodzi z mojego doświadczenia z literaturą .NET i GC. Powinien dobrze uogólniać na inne platformy (przynajmniej te, które mają znaczący GC).

  • Testy różnego rodzaju. Chcesz znanego zarządzanego stanu sterty na początku testu porównawczego, aby GC nie uruchamiał się losowo podczas testów porównawczych. Kiedy powtarzasz test, potrzebujesz tej samej liczby i ilości pracy GC w każdym powtórzeniu.
  • Nagłe uwolnienie zasobów. Na przykład zamknięcie znacznego okna GUI lub odświeżenie pamięci podręcznej (a tym samym zwolnienie starej potencjalnie dużej zawartości pamięci podręcznej). GC nie może tego wykryć, ponieważ wszystko, co robisz, to ustawienie odwołania na null. Fakt, że powoduje to osierocenie wykresu całego obiektu, nie jest łatwo wykrywalny.
  • Uwolnienie niezarządzanych zasobów, które wyciekły . Oczywiście to nigdy nie powinno się zdarzyć, ale widziałem przypadki, w których biblioteka innej firmy wyciekła (np. Obiekty COM). Deweloper zmuszony był czasami zaindukować kolekcję.
  • Interaktywne aplikacje, takie jak gry . Podczas gry gry mają bardzo ścisły budżet na klatkę (60 Hz => 16 ms na klatkę). Aby uniknąć czkawek, potrzebujesz strategii radzenia sobie z GC. Jedną z takich strategii jest maksymalne opóźnienie GC G2 i wymuszenie ich w odpowiednim czasie, takim jak ekran ładowania lub przerywnik filmowy. GC nie może wiedzieć, kiedy jest najlepszy taki moment.
  • Ogólnie kontrola opóźnień . Niektóre aplikacje internetowe wyłączają GC i okresowo uruchamiają kolekcję G2, gdy są wyłączane z rotacji modułu równoważenia obciążenia. W ten sposób opóźnienie G2 nigdy nie zostanie ujawnione użytkownikowi.

Jeśli twoim celem jest przepustowość, tym rzadziej GC, tym lepiej. W takich przypadkach wymuszenie kolekcji nie może mieć pozytywnego wpływu (z wyjątkiem dość wymyślnych problemów, takich jak zwiększenie wykorzystania pamięci podręcznej procesora przez usunięcie martwych obiektów umieszczonych między nimi). Pobieranie partii jest bardziej wydajne dla wszystkich kolektorów, o których wiem. W przypadku aplikacji produkcyjnej używającej pamięci w stanie ustalonym indukowanie GC nie pomaga.

Podane powyżej przykłady dotyczą spójności i ograniczenia wykorzystania pamięci. W takich przypadkach indukowane GC mogą mieć sens.

Wydaje się, że istnieje szeroko rozpowszechniony pogląd, że GC jest boską istotą, która wywołuje zbiór, gdy jest to rzeczywiście optymalne. Żaden GC, o którym wiem, nie jest tak wyrafinowany i naprawdę bardzo trudno jest być optymalnym dla GC. GC wie mniej niż programista. Jego heurystyka oparta jest na licznikach pamięci i takich rzeczach, jak szybkość zbierania i tak dalej. Heurystyka jest zwykle dobra, ale nie rejestruje nagłych zmian w zachowaniu aplikacji, takich jak zwolnienie dużej ilości zarządzanej pamięci. Jest również ślepy na niezarządzane zasoby i wymagania dotyczące opóźnień.

Uwaga: koszty GC różnią się w zależności od wielkości sterty i liczby referencji na sterty. Na małej kupce koszt może być bardzo mały. Widziałem szybkości zbierania G2 z .NET 4.5 z 1-2 GB / s w aplikacji produkcyjnej o wielkości sterty 1 GB.


W przypadku kontroli opóźnienia myślę, że zamiast robić to okresowo, możesz to zrobić w razie potrzeby (tj. Gdy zużycie pamięci wzrośnie powyżej pewnego progu).
Paŭlo Ebermann

3
+1 za drugi do ostatniego akapitu. Niektóre osoby mają takie same zdanie na temat kompilatorów i szybko nazywają prawie wszystko „przedwczesną optymalizacją”. Zwykle mówię im coś podobnego.
Honza Brabec

2
+1 również dla tego akapitu. Uważam za szokujące, że ludzie myślą, że program komputerowy napisany przez kogoś innego musi koniecznie lepiej rozumieć cechy wydajnościowe swojego programu niż oni sami.
Mehrdad

1
@HonzaBrabec Problem jest taki sam w obu przypadkach: jeśli uważasz, że znasz się lepiej niż GC lub kompilator, to bardzo łatwo jest zrobić sobie krzywdę. Jeśli faktycznie wiesz więcej, optymalizujesz tylko wtedy, gdy wiesz, że nie jest to przedwczesne.
sick

27

Zgodnie z ogólną zasadą śmieciarz będzie zbierał, gdy napotka „presję pamięci”, i dobrym pomysłem jest, aby nie zbierać go w innym czasie, ponieważ możesz spowodować problemy z wydajnością lub nawet zauważalne przerwy w wykonywaniu programu. I w rzeczywistości pierwszy punkt zależy od drugiego: przynajmniej dla pokoleniowego śmieciarza działa przynajmniej wydajniej, im wyższy jest stosunek śmieci do dobrych obiektów, więc w celu zminimalizowania czasu poświęcanego na wstrzymywanie programu , musi się odwlekać i pozwolić, aby śmieci gromadziły się jak najwięcej.

Odpowiednim czasem na ręczne wywołanie modułu wyrzucania elementów bezużytecznych jest wtedy, gdy skończysz robić coś, co 1) prawdopodobnie spowodowało powstanie dużej ilości śmieci, a 2) użytkownik oczekuje, że poświęci trochę czasu i pozostawi system bez odpowiedzi tak czy siak. Klasyczny przykład to koniec ładowania czegoś dużego (dokument, model, nowy poziom itp.)


12

Jedną rzeczą, o której nikt nie wspominał, jest to, że chociaż Windows GC jest niesamowicie dobry, GC na Xboksie jest śmieciem (gra słów zamierzona) .

Dlatego podczas kodowania gry XNA, która ma być uruchomiona na XBox, niezwykle ważne jest, aby zbieranie śmieci było odpowiednie dla odpowiednich chwil, w przeciwnym razie będziesz mieć okropne sporadyczne czkawki FPS. Ponadto w XBox często używa się go structznacznie częściej niż zwykle, aby zminimalizować liczbę obiektów, które należy zebrać w śmietniku.


4

Odśmiecanie jest przede wszystkim narzędziem do zarządzania pamięcią. W związku z tym śmieciarze będą zbierać, gdy pojawi się presja pamięci.

Nowoczesne śmieciarki są bardzo dobre i stają się coraz lepsze, więc jest mało prawdopodobne, że można je poprawić, zbierając ręcznie. Nawet jeśli dzisiaj możesz ulepszyć rzeczy, może się zdarzyć, że przyszłe ulepszenie wybranego śmieciarza sprawi, że Twoja optymalizacja będzie nieskuteczna, a nawet przyniesie efekt przeciwny do zamierzonego.

Jednak śmietniki zwykle nie próbują zoptymalizować wykorzystania zasobów innych niż pamięć. W środowiskach śmieciowych najbardziej wartościowe zasoby inne niż pamięć mają closemetodę lub podobną metodę, ale w niektórych przypadkach nie jest to możliwe z jakiegoś powodu, na przykład zgodność z istniejącym interfejsem API.

W takich przypadkach sensowne może być ręczne wywołanie odśmiecania, gdy wiadomo, że używany jest cenny zasób inny niż pamięć.

RMI

Jednym konkretnym przykładem tego jest zdalne wywołanie metody Java. RMI to zdalna biblioteka wywołań procedur. Zwykle masz serwer, który udostępnia różne obiekty do użytku przez klientów. Jeśli serwer wie, że obiekt nie jest używany przez żadnego klienta, wówczas obiekt ten kwalifikuje się do odśmiecania.

Jednak jedynym sposobem, w jaki serwer wie, jest to, jeśli klient mu to powie, a klient powie serwerowi, że nie potrzebuje już obiektu, gdy klient zbierze śmieci, cokolwiek z nich korzysta.

Stanowi to problem, ponieważ klient może mieć dużo wolnej pamięci, więc nie może zbyt często uruchamiać czyszczenia pamięci. Tymczasem serwer może mieć w pamięci wiele nieużywanych obiektów, których nie może gromadzić, ponieważ nie wie, że klient ich nie używa.

Rozwiązaniem w RMI jest okresowe uruchamianie śmiecia przez klienta, nawet gdy ma dużo wolnej pamięci, aby zapewnić szybkie gromadzenie obiektów na serwerze.


„W takich przypadkach sensowne może być ręczne wywołanie odśmiecania, gdy wiadomo, że używany jest cenny zasób inny niż pamięć” - jeśli używany jest zasób inny niż pamięć, należy użyć usingbloku lub w inny sposób wywołać Closemetodę upewnij się, że zasób zostanie odrzucony jak najszybciej. Poleganie na GC w celu czyszczenia zasobów innych niż pamięć jest zawodne i powoduje różnego rodzaju problemy (szczególnie w przypadku plików, które muszą być zablokowane, aby można było je otworzyć tylko raz).
Jules

I jak stwierdzono w odpowiedzi, gdy closemetoda jest dostępna (lub zasób może być używany z usingblokiem), jest to właściwe podejście. Odpowiedź dotyczy szczególnie rzadkich przypadków, w których mechanizmy te nie są dostępne.
James_pic

Moim osobistym zdaniem jest to, że każdy interfejs, który zarządza zasobem innym niż pamięć, ale nie zapewnia metody zamykania, jest interfejsem, którego nie należy używać , ponieważ nie ma możliwości niezawodnego korzystania z niego.
Jules

@Jules Zgadzam się, ale czasami jest to nieuniknione. Czasami abstrakcje przeciekają, a korzystanie z nieszczelnej abstrakcji jest lepsze niż nieużywanie abstrakcji. Czasami musisz pracować ze starszym kodem, który wymaga składania obietnic, których nie możesz dotrzymać. Tak, jest to rzadkie i należy tego unikać, jeśli to możliwe, i istnieje powód, dla którego istnieją wszystkie te ostrzeżenia wymuszające zbieranie śmieci, ale takie sytuacje się pojawiają, a OP pytał, jak te sytuacje wyglądają - na co odpowiedziałem .
James_pic

2

Najlepszą praktyką jest nie wymuszanie wyrzucania elementów bezużytecznych w większości przypadków. (Każdy system, nad którym pracowałem, wymuszał zbieranie śmieci, podkreślał problemy, które, jeśli rozwiązane, usunęłyby potrzebę wymuszania zbierania śmieci i znacznie przyspieszyły system).

Istnieje kilka przypadków , w których wiesz więcej na temat wykorzystania pamięci niż robi to moduł czyszczenia pamięci. Jest to mało prawdopodobne w przypadku aplikacji dla wielu użytkowników lub usługi, która odpowiada na więcej niż jedno żądanie na raz.

Jednak w niektórych procesach przetwarzania wsadowego wiesz więcej niż GC. Np. Rozważ aplikację, która.

  • Podano listę nazw plików w wierszu poleceń
  • Przetwarza pojedynczy plik, a następnie wypisuje wynik do pliku wyników.
  • Podczas przetwarzania pliku tworzy wiele powiązanych ze sobą obiektów, których nie można zebrać, dopóki przetwarzanie pliku nie zostanie zakończone (np. Parsowanie drzewa)
  • Nie utrzymuje stanu dopasowania między przetwarzanymi plikami .

Być może będziesz w stanie (po starannym) przetestować, czy powinieneś wymusić pełne wyrzucanie elementów bezużytecznych po przetworzeniu każdego pliku.

Kolejne przypadki to usługa, która budzi się co kilka minut w celu przetworzenia niektórych przedmiotów i nie utrzymuje żadnego stanu podczas snu . Następnie zmuszając pełną kolekcję tuż przed pójściem spać może się opłacać.

Zastanawiam się nad wymuszeniem kolekcji tylko wtedy, gdy wiem, że ostatnio utworzono wiele obiektów i do których odwołuje się obecnie niewiele obiektów.

Wolę mieć interfejs API do wyrzucania elementów bezużytecznych, gdybym mógł dać mu wskazówki na temat tego typu rzeczy bez konieczności wymuszania na mnie GC.

Zobacz także „ Ciekawostki o wydajności Rico Mariani


2

Istnieje kilka przypadków, w których możesz chcieć samodzielnie wywołać gc ().

  • [ Niektórzy twierdzą, że to nie jest dobre, ponieważ może promować obiekty w przestrzeni starszej generacji, co zgadzam się, że nie jest dobre. Jednak NIE zawsze jest prawdą, że zawsze będą obiekty, które można promować. Z pewnością możliwe jest, że po tym gc()wywołaniu pozostanie bardzo niewiele obiektów, nie mówiąc już o przeniesieniu ich w przestrzeń starszej generacji ]. Kiedy zamierzasz stworzyć dużą kolekcję obiektów i zużyć dużo pamięci. Po prostu chcesz wyczyścić jak najwięcej miejsca, jak to możliwe. To tylko zdrowy rozsądek. Po gc()ręcznym wywołaniu nie będzie zbędnego sprawdzania wykresu referencyjnego w części tej dużej kolekcji obiektów, które ładujesz do pamięci. Krótko mówiąc, jeśli uruchomisz gc()przed załadowaniem dużo do pamięci,gc() indukowane podczas ładowania zdarza się rzadziej co najmniej raz, gdy ładowanie rozpoczyna tworzenie ciśnienia pamięci.
  • Po zakończeniu ładowania dużej kolekcjidużyobiekty, a raczej nie załadujesz więcej obiektów do pamięci. Krótko mówiąc, przechodzisz od fazy tworzenia do fazy używania. Dzwoniąc w gc()zależności od implementacji, używana pamięć zostanie spakowana, co znacznie poprawia lokalizację pamięci podręcznej. Spowoduje to znaczną poprawę wydajności, której nie uzyskasz dzięki profilowaniu .
  • Podobnie jak w przypadku pierwszego, ale z punktu widzenia tego, że jeśli to zrobisz, gc()a implementacja zarządzania pamięcią obsługuje, stworzysz znacznie lepszą ciągłość swojej pamięci fizycznej. To znowu sprawia, że ​​nowa duża kolekcja obiektów jest bardziej ciągła i zwarta, co z kolei poprawia wydajność

1
Czy ktoś może wskazać przyczynę głosowania? Sam nie wiem wystarczająco, aby ocenić odpowiedź (na pierwszy rzut oka ma to dla mnie sens).
Omega,

1
Zgaduję, że masz głos negatywny za trzeci punkt. Potencjalnie również za powiedzenie „To tylko zdrowy rozsądek”.
immibis

2
Kiedy tworzysz dużą kolekcję obiektów, GC powinien być wystarczająco inteligentny, aby wiedzieć, czy kolekcja jest potrzebna. To samo, gdy pamięć wymaga kompaktowania. Poleganie na GC w celu optymalizacji lokalizacji pamięci powiązanych obiektów nie wydaje się niezawodne. Myślę, że możesz znaleźć inne rozwiązania (struct, niebezpieczne, ...). (Nie jestem zwycięzcą).
Guillaume,

3
Twoim pierwszym pomysłem na dobry czas jest po prostu zła rada. Szanse są duże, że ostatnio była kolekcja, więc twoja próba ponownego zebrania będzie po prostu arbitralnie promować przedmioty do późniejszych generacji, co prawie zawsze jest złe. Późniejsze pokolenia mają kolekcje, które zaczynają się dłużej, a zwiększenie ich sterty „w celu wyczyszczenia jak największej ilości miejsca” powoduje, że jest to bardziej problematyczne. Plus, jeśli masz zamiar zwiększyć presję pamięci z ładunkiem, prawdopodobnie zaczniesz indukować kolekcje, które będą działać wolniej, ponieważ zwiększona Gen1 / 2
Jimmy Hoffa

2
By calling gc() depending on implementation, the memory in used will be compacted which massively improves cache locality. This will result in massive improve in performance that you will not get from profiling.Jeśli przydzielisz tonę obiektów z rzędu, są one już zagęszczone. Jeśli już, zbieranie śmieci może je nieco przetasować. Tak czy inaczej, użycie gęstszych struktur danych i nieprzeskakiwanie losowo w pamięci będzie miało większy wpływ. Jeśli używasz naiwnej listy połączonej jeden element na węzeł, to żadna ilość ręcznych sztuczek GC nie nadrobi tego.
Doval,

2

Przykład z prawdziwego świata:

Miałem aplikację internetową, która korzystała z bardzo dużego zestawu danych, które rzadko się zmieniały i do których trzeba było uzyskać dostęp bardzo szybko (wystarczająco szybko, aby uzyskać odpowiedź za naciśnięciem klawisza przez AJAX).

Rzeczą oczywistą jest załadowanie odpowiedniego wykresu do pamięci i dostęp do niego stamtąd, a nie do bazy danych, aktualizowanie wykresu po zmianie DB.

Ale ponieważ jest bardzo duży, naiwne obciążenie zajęłoby co najmniej 6 GB pamięci, a dane miałyby wzrosnąć w przyszłości. (Nie mam dokładnych danych, gdy stało się jasne, że mój komputer o pojemności 2 GB próbuje poradzić sobie z co najmniej 6 GB, miałem wszystkie pomiary, których potrzebowałem, aby wiedzieć, że to nie zadziała).

Na szczęście w tym zestawie danych znajdowała się duża liczba obiektów niezmiennych w popsicle, które były takie same; gdy już zorientowałem się, że pewna partia była taka sama jak inna partia, mogłem alias jednego odniesienia do drugiego, pozwalając na zebranie dużej ilości danych, a zatem zmieścić wszystko w mniej niż pół giganta.

Wszystko dobrze i dobrze, ale do tego wciąż przebija się ponad 6 GB obiektów w ciągu około pół minuty, aby dostać się do tego stanu. Pozostawiony sam sobie, GC nie poradził sobie; skok aktywności w stosunku do zwykłego schematu aplikacji (znacznie mniejszy przy zwolnieniach na sekundę) był zbyt ostry.

Okresowe dzwonienie GC.Collect()podczas procesu kompilacji oznaczało, że wszystko działało bezproblemowo. Oczywiście nie zadzwoniłem ręcznie GC.Collect()przez resztę czasu działania aplikacji.

Ten rzeczywisty przypadek jest dobrym przykładem wytycznych, kiedy powinniśmy użyć GC.Collect():

  1. Używaj z relatywnie rzadkim przypadkiem udostępnienia wielu obiektów do gromadzenia (udostępniono wartość megabajtów, a to tworzenie wykresów było bardzo rzadkim przypadkiem przez cały okres użytkowania aplikacji (około jednej minuty tygodniowo).
  2. Zrób to, gdy utrata wydajności jest stosunkowo dopuszczalna; stało się to tylko przy uruchomieniu aplikacji. (Innym dobrym przykładem tej zasady jest między poziomami w trakcie gry lub innymi punktami w grze, w których gracze nie będą zmartwieni przez chwilę przerwy).
  3. Profil, aby upewnić się, że naprawdę jest poprawa. (Całkiem proste; „Działa” prawie zawsze bije „nie działa”).

Przez większość czasu, gdy myślałem, że mogę mieć przypadek, w którym GC.Collect()warto zadzwonić, ponieważ zastosowanie miały punkty 1 i 2, punkt 3 sugerował, że pogorszyło to sytuację lub przynajmniej poprawiło sytuację (i przy niewielkiej lub żadnej poprawie skłaniaj się ku nieprzekazywaniu połączeń, ponieważ takie podejście może okazać się lepsze w trakcie życia aplikacji).


0

Mam zastosowanie do usuwania śmieci, co jest nieco niekonwencjonalne.

Istnieje ta błędna praktyka, która jest niestety bardzo rozpowszechniona w świecie C #, polegająca na implementacji usuwania obiektów za pomocą brzydkiego, niezgrabnego, nieeleganckiego i podatnego na błędy idiomu znanego jako pozbywanie się IDisposable . MSDN opisuje to szczegółowo , a wiele osób przysięga na to, podąża za nim religijnie, spędza godziny na godziny, dyskutując dokładnie, jak należy to zrobić itp.

(Zwróć uwagę, że to, co nazywam tutaj brzydkim, nie jest samym wzorem usuwania obiektów; to, co nazywam brzydkim, to szczególny IDisposable.Dispose( bool disposing )idiom.)

Ten idiom został wymyślony, ponieważ podobno niemożliwe jest zagwarantowanie, że niszczyciel twoich obiektów będzie zawsze wywoływany przez moduł wyrzucania elementów bezużytecznych w celu oczyszczenia zasobów, więc ludzie wykonują czyszczenie zasobów wewnątrz IDisposable.Dispose(), a na wypadek, gdyby zapomnieli, spróbują jeszcze raz w destruktorze. Wiesz, na wszelki wypadek.

Ale wtedy możesz IDisposable.Dispose()mieć zarówno zarządzane, jak i niezarządzane obiekty do oczyszczenia, ale zarządzanych nie można wyczyścić, gdy IDisposable.Dispose()jest wywoływany z poziomu destruktora, ponieważ zostały one już zajęte przez śmietnik w tym momencie, więc jest to potrzeba osobnej Dispose()metody, która akceptuje bool disposingflagę, aby wiedzieć, czy należy usuwać zarówno obiekty zarządzane, jak i niezarządzane, czy tylko te niezarządzane.

Przepraszam, ale to jest po prostu szalone.

Opieram się na aksjomacie Einsteina, który mówi, że rzeczy powinny być tak proste, jak to możliwe, ale nie prostsze. Oczywiście nie możemy pominąć czyszczenia zasobów, więc najprostsze możliwe rozwiązanie musi obejmować przynajmniej to. Kolejne najprostsze rozwiązanie polega na tym, aby zawsze pozbywać się wszystkiego dokładnie w tym samym czasie, w którym ma on zostać unieszkodliwiony, bez komplikowania rzeczy, polegając na niszczycielu jako alternatywnym rozwiązaniu awaryjnym.

Teraz, ściśle mówiąc, jest to oczywiście niemożliwe, aby zagwarantować, że żaden programista nigdy nie popełnia błąd zapominając powołać IDisposable.Dispose(), ale to, co można zrobić, to użyć destruktora aby nadrobić ten błąd. To naprawdę bardzo proste: wszystko, co musi zrobić destruktor, to wygenerować wpis dziennika, jeśli wykryje, że disposedflaga obiektu jednorazowego nigdy nie była ustawiona true. Zatem użycie destruktora nie jest integralną częścią naszej strategii usuwania, ale jest naszym mechanizmem zapewnienia jakości. A ponieważ jest to test tylko w trybie debugowania, możemy umieścić cały destruktor w #if DEBUGbloku, więc nigdy nie ponosimy żadnej kary za zniszczenie w środowisku produkcyjnym. ( IDisposable.Dispose( bool disposing )Idiom nakazuje toGC.SuppressFinalize() należy wywoływać właśnie w celu zmniejszenia narzutu związanego z finalizacją, ale dzięki mojemu mechanizmowi można całkowicie uniknąć tego narzutu w środowisku produkcyjnym).

Sprowadza się to do argumentu wiecznego twardego błędu vs. miękkiego błędu : IDisposable.Dispose( bool disposing )idiom jest podejściem opartym na miękkim błędzie i stanowi próbę umożliwienia programistom zapomnienia wywołania Dispose()bez awarii systemu, jeśli to możliwe. Metoda twardego błędu mówi, że programista musi zawsze upewnić się, że Dispose()zostanie wywołany. Karą zwykle zalecaną przez podejście oparte na błędzie w większości przypadków jest niepowodzenie asercji, ale w tym konkretnym przypadku robimy wyjątek i zmniejszamy karę do zwykłego wydania wpisu dziennika błędów.

Tak więc, aby ten mechanizm działał, wersja DEBUG naszej aplikacji musi wykonać pełne usuwanie śmieci przed zakończeniem, aby zagwarantować, że zostaną wywołane wszystkie destruktory, a tym samym złapać wszelkie IDisposableobiekty, o których zapomnieliśmy się pozbyć.


Now, strictly speaking, it is of course impossible to guarantee that no programmer will ever make the mistake of forgetting to invoke IDisposable.Dispose()W rzeczywistości tak nie jest, choć nie sądzę, że C # jest w stanie to zrobić. Nie narażaj zasobu; zamiast tego podaj DSL opisujący wszystko, co z nim zrobisz (w zasadzie monadę), a także funkcję, która pozyskuje zasób, robi rzeczy, uwalnia je i zwraca wynik. Sztuką jest użycie systemu typów, aby zagwarantować, że jeśli ktoś przemyci odniesienie do zasobu, nie będzie można go użyć w innym wywołaniu funkcji uruchamiania.
Doval,

2
Problem z Dispose(bool disposing)(który nie jest zdefiniowany) IDisposablepolega na tym, że służy on do czyszczenia obiektów zarządzanych i niezarządzanych, które obiekt ma jako pole (lub w inny sposób jest za nie odpowiedzialny), co rozwiązuje niewłaściwy problem. niezarządzane obiekty w zarządzanym obiekcie bez innych obiektów jednorazowych, o które należy się martwić, wówczas wszystkie Dispose()metody będą albo jedną z tych metod (w razie potrzeby poproszę finalizator o takie samo czyszczenie), albo będą miały do ​​dyspozycji tylko obiekty zarządzane (nie dysponujemy finalizatorem w ogóle), a potrzeba bool disposingzniknie
Jon Hanna

-1 zła rada, ponieważ tak naprawdę działa finalizacja. Całkowicie zgadzam się z twoją dispose(disposing)tezą, że idiomem jest terribad, ale mówię tak, ponieważ ludzie tak często używają tej techniki i finalizatorów, gdy mają tylko zasoby zarządzane ( DbConnectionna przykład obiekt jest zarządzany , nie jest zarządzany , nie jest połączony), a TYLKO POWINIENEŚ KAŻDY WDRAŻA FINALIZATOR Z NIEZARZĄDZANYM, PINVOKED, COM MARSHALLED LUB NIEBEZPIECZNYM KODEM . Opisałem powyżej w mojej odpowiedzi, jak bardzo drogie są finalizatory, nie używaj ich, chyba że masz niezarządzane zasoby w swojej klasie.
Jimmy Hoffa

2
Prawie chcę dać ci +1, tylko dlatego, że potępiasz coś, co tak wielu ludzi dispose(dispoing)uważa za podstawową ważną rzecz w tym idiomie, ale prawda jest taka, że ​​jest to tak powszechne, ponieważ ludzie tak boją się materiałów GC, że coś tak niezwiązanego jak że ( disposepowinien mieć związek z GC) zasługuje na to, aby po prostu wziąć przepisany lek, nawet go nie badając. Dobrze, że go sprawdziłeś, ale tęskniłeś za największą całością (zachęca to bardziej finalistów farrr, niż powinni być)
Jimmy Hoffa

1
@JimmyHoffa dziękuję za wkład. Zgadzam się, że finalizator powinien normalnie być używany tylko do uwalniania niezarządzanych zasobów, ale czy nie zgadzasz się, że w kompilacji DEBUG ta zasada nie ma zastosowania, i że w kompilacji DEBUG powinniśmy mieć swobodę używania finalizatorów do łapania błędów? To wszystko, co sugeruję tutaj, więc nie rozumiem, dlaczego masz z tym problem. Zobacz także programmers.stackexchange.com/questions/288715/..., aby uzyskać dłuższe wyjaśnienie tego podejścia po stronie Java.
Mike Nakis,

0

Czy możesz mi powiedzieć w jakim scenariuszu wymuszenie zbierania śmieci jest dobrym lub rozsądnym pomysłem? Nie pytam o przypadki specyficzne dla C #, ale raczej wszystkie języki programowania, które mają moduł wyrzucania elementów bezużytecznych. Wiem, że nie możesz wymuszać GC na wszystkie języki, takie jak Java, ale załóżmy, że możesz.

Mówiąc bardzo teoretycznie i nie uwzględniając problemów, takich jak niektóre implementacje GC spowalniające rzeczy podczas ich cykli zbierania, największym scenariuszem, jaki wymyślam, aby wymusić odśmiecanie, jest oprogramowanie o znaczeniu krytycznym, w którym logiczne wycieki są lepsze niż wiszące awarie wskaźnika, np. Z powodu awarii w nieoczekiwanych czasach może kosztować życie ludzkie lub coś w tym rodzaju.

Jeśli spojrzysz na niektóre gry typu shoddier indie napisane przy użyciu języków GC, takie jak gry Flash, wyciekają jak szalone, ale się nie zawieszają. Gra może zająć dziesięć razy więcej pamięci niż 20 minut, ponieważ część kodu w grze zapomniała ustawić wartość zerową lub usunąć ją z listy, a liczba klatek na sekundę może zacząć spadać, ale gra nadal działa. Podobna gra napisana przy użyciu tandetnego kodu C lub C ++ może ulec awarii w wyniku dostępu do wiszących wskaźników w wyniku tego samego rodzaju błędu zarządzania zasobami, ale nie wycieknie tak bardzo.

W przypadku gier awaria może być lepsza w tym sensie, że można ją szybko wykryć i naprawić, ale w przypadku programu o krytycznym znaczeniu, awaria w zupełnie nieoczekiwanych momentach może kogoś zabić. Myślę więc, że główne przypadki, w których nie występują awarie lub niektóre inne formy bezpieczeństwa są absolutnie niezbędne, a przeciek logiczny jest względnie trywialny.

Główny scenariusz, w którym myślę, że zmuszanie GC do złego działania jest złe, dotyczy rzeczy, w których logiczny wyciek jest w rzeczywistości mniej preferowany niż awaria. Na przykład w przypadku gier awaria nie musi nikogo zabić i może zostać łatwo złapana i naprawiona podczas testów wewnętrznych, podczas gdy logiczny wyciek może pozostać niezauważony nawet po wysłaniu produktu, chyba że jest tak poważny, że uniemożliwia grę w ciągu kilku minut . W niektórych domenach łatwiejsza do odtworzenia awaria występująca podczas testowania jest czasem lepsza niż wyciek, którego nikt nie zauważa natychmiast.

Innym przypadkiem, w którym mogę wymyślić wymuszenie GC w zespole, jest bardzo krótkotrwały program, taki jak po prostu wykonanie z wiersza poleceń, które wykonuje jedno zadanie, a następnie wyłącza się. W takim przypadku czas życia programu jest zbyt krótki, aby jakikolwiek logiczny wyciek nie był trywialny. Logiczne wycieki, nawet w przypadku dużych zasobów, zwykle stają się problematyczne dopiero kilka godzin lub minut po uruchomieniu oprogramowania, więc oprogramowanie, które ma być uruchamiane tylko przez 3 sekundy, prawdopodobnie nie będzie miało problemów z logicznymi przeciekami, i może mieć duże znaczenie prościej jest pisać tak krótkotrwałe programy, jeśli zespół użył GC.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.