Najlepsze praktyki dotyczące wymuszania wyrzucania elementów bezużytecznych w języku C #

118

Z mojego doświadczenia wynika, że ​​większość ludzi powie ci, że wymuszanie czyszczenia pamięci jest nierozsądne, ale w niektórych przypadkach, gdy pracujesz z dużymi obiektami, które nie zawsze są zbierane w generacji 0, ale gdzie problemem jest pamięć, jest czy można wymusić zbieranie? Czy istnieją najlepsze praktyki, aby to robić?

Echostorm
źródło

Odpowiedzi:

112

Najlepszą praktyką jest nie wymuszanie wyrzucania elementów bezużytecznych.

Według MSDN:

„Można wymusić wyrzucanie elementów bezużytecznych przez wywołanie funkcji Collect, ale w większości przypadków należy tego unikać, ponieważ może to powodować problemy z wydajnością”.

Jeśli jednak możesz niezawodnie przetestować swój kod, aby potwierdzić, że wywołanie Collect () nie będzie miało negatywnego wpływu, kontynuuj ...

Po prostu postaraj się, aby obiekty zostały wyczyszczone, gdy już ich nie potrzebujesz. Jeśli masz obiekty niestandardowe, skorzystaj z instrukcji „using” i interfejsu IDisposable.

Ten link zawiera kilka dobrych praktycznych porad dotyczących zwalniania pamięci / czyszczenia pamięci itp .:

http://msdn.microsoft.com/en-us/library/66x5fx1b.aspx

Mark Ingram
źródło
3
Możesz także ustawić różne msdn.microsoft.com/en-us/library/bb384202.aspx
David d C e Freitas
4
Jeśli obiekty wskazują na niezarządzaną pamięć, możesz powiadomić moduł odśmiecania pamięci za pośrednictwem interfejsu GC.AddMemoryPressure Api ( msdn.microsoft.com/en-us/library/… ). Daje to odśmiecaczowi więcej informacji o systemie bez ingerencji w algorytmy zbierania.
Govert
+1: * spójrz na użycie „instrukcji using” i interfejsu IDisposable. * Nawet nie rozważałbym wymuszania, chyba że w ostateczności - dobra rada (czytaj jako „wyłączenie odpowiedzialności”). Wymuszam jednak zbieranie w teście jednostkowym, aby zasymulować utratę aktywnego odwołania w operacji zaplecza - ostatecznie rzucając plik TargetOfInvocationNullException.
IAbstract
33

Spójrz na to w ten sposób - czy efektywniej jest wyrzucać śmieci kuchenne, gdy poziom kosza na śmieci wynosi 10%, czy też pozwolić mu się zapełnić przed wyjęciem?

Nie pozwalając mu się zapełnić, tracisz czas na chodzenie do i z kosza na śmieci na zewnątrz. Jest to analogiczne do tego, co dzieje się, gdy działa wątek GC - wszystkie zarządzane wątki są zawieszane, gdy jest uruchomiony. I jeśli się nie mylę, wątek GC może być współużytkowany przez wiele domen AppDomains, więc czyszczenie pamięci wpływa na wszystkie z nich.

Oczywiście możesz spotkać się z sytuacją, w której w najbliższym czasie nie będziesz niczego dodawać do kosza - powiedzmy, jeśli wybierasz się na wakacje. Wtedy dobrze byłoby wyrzucić śmieci przed wyjściem.

MOŻE to być jeden raz, kiedy wymuszenie GC może pomóc - jeśli twój program jest bezczynny, używana pamięć nie jest usuwana z pamięci, ponieważ nie ma przydziałów.

Maxam
źródło
5
Gdybyś miał dziecko, które umarłoby, gdybyś zostawił je na dłużej niż minutę, a miałbyś tylko minutę na uprzątnięcie śmieci, to za każdym razem chciałbyś zrobić trochę, a nie naraz. Niestety metoda GC :: Collect () nie jest tym szybsza, im częściej ją wywołujesz. Więc dla silnika czasu rzeczywistego, jeśli nie możesz po prostu użyć mechanizmu usuwania i pozwolić GC zebrać dane, nie powinieneś używać systemu zarządzanego - zgodnie z moją odpowiedzią (prawdopodobnie poniżej tego, lol).
Jin
1
W moim przypadku używam algorytmu A * (najkrótszej ścieżki) POWTARZANIE w celu dostrojenia wydajności ... który w produkcji będzie uruchamiany tylko raz (na każdej „mapie”). Dlatego chcę, aby GC było wykonywane przed każdą iteracją, poza moim „blokiem mierzonym wydajnością”, ponieważ uważam, że dokładniej modeluje sytuację w produkcji, w której GC mógłby / powinien być wymuszony po przejściu przez każdą „mapę”.
corlettk
Wywołanie GC przed mierzonym blokiem w rzeczywistości nie modeluje sytuacji w produkcji, ponieważ w produkcji GC zostanie wykonane w nieprzewidywalnych czasach. Aby to złagodzić, należy wykonać długi pomiar, który będzie obejmował kilka wykonań GC, i uwzględnić piki podczas GC w analizie i statystykach.
Biegał
32

W większości przypadków najlepszym rozwiązaniem jest niewymuszanie wyrzucania elementów bezużytecznych. (Każdy system, nad którym pracowałem, wymuszał zbieranie śmieci, miał podkreślane problemy, których rozwiązanie wyeliminowałoby potrzebę wymuszania zbierania śmieci i znacznie przyspieszyło system.)

Jest kilka przypadków , w których wiesz więcej na temat użycia pamięci niż garbage collector. 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 naraz.

Jednak w niektórych procesach wsadowych wiesz więcej niż GC. Np. Rozważ taką aplikację.

  • Otrzymuje listę nazw plików w linii poleceń
  • Przetwarza pojedynczy plik, a następnie zapisuje wynik do pliku wyników.
  • Podczas przetwarzania pliku tworzy wiele powiązanych ze sobą obiektów, których nie można zebrać do czasu zakończenia przetwarzania pliku (np. Drzewo parsowania)
  • Nie zachowuje wielu stanów między przetwarzanymi plikami .

Państwo może być w stanie dokonać przypadek (po ostrożnym) badania, które powinny wymusić pełną kolekcję śmieci po trzeba przetworzyć każdy plik.

Innym przypadkiem jest usługa, która budzi się co kilka minut w celu przetworzenia niektórych elementó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ć.

Jedynym przypadkiem, w którym rozważałbym wymuszenie kolekcji, jest sytuacja, w której wiem, że ostatnio utworzono wiele obiektów i obecnie istnieją odwołania do bardzo niewielu obiektów.

Wolałbym mieć API do zbierania śmieci, kiedy mógłbym dać mu wskazówki na temat tego typu rzeczy bez konieczności wymuszania GC samodzielnie.

Zobacz też „ Ciekawostki o występach Rico Marianiego

Ian Ringrose
źródło
2
Analogia: Przedszkole (system) przechowuje kredki (zasoby). W zależności od liczby dzieci (zadań) i niedoboru kolorów, nauczyciel (.Net) decyduje, jak przydzielić dzieciom i podzielić się nimi. Gdy wymagany jest rzadki kolor, nauczyciel może przydzielić z puli lub poszukać takiego, który nie jest używany. Nauczyciel może według własnego uznania okresowo zbierać niewykorzystane kredki (zbieranie śmieci), aby utrzymać porządek (optymalizować wykorzystanie zasobów). Generalnie rodzic (programista) nie może z góry określić najlepszej polityki porządkowania kredek w klasie. Jest mało prawdopodobne, aby zaplanowana drzemka jednego dziecka była dobrym momentem, aby zakłócić kolorystykę innych dzieci.
AlanK
1
@AlanK, podoba mi się to, ponieważ kiedy dzieci idą do domu na cały dzień, jest to bardzo dobry czas dla pomocnika nauczyciela, aby dobrze posprzątać bez przeszkadzania dzieciom. (Ostatnie systemy, nad którymi pracowałem, właśnie ponownie uruchomiły proces obsługi w takich momentach, bez wymuszenia GC.)
Ian Ringrose,
21

Myślę, że przykład podany przez Rico Marianiego był dobry: wszczęcie GC może być właściwe, jeśli nastąpi znacząca zmiana w stanie aplikacji. Na przykład w edytorze dokumentów może być OK, aby wyzwolić GC, gdy dokument jest zamknięty.

denis phillips
źródło
2
Lub tuż przed otwarciem dużego, ciągłego obiektu, który wykazał historię awarii i nie przedstawia skutecznego rozwiązania dla zwiększenia jego szczegółowości.
crokusek
17

Istnieje kilka ogólnych wskazówek dotyczących programowania, które są bezwzględne. W połowie przypadków, gdy ktoś mówi „robisz to źle”, po prostu wypowiada pewną ilość dogmatów. W C dawniej obawiał się rzeczy, takich jak samomodyfikujący się kod lub wątki, w językach GC wymusza GC lub alternatywnie uniemożliwia uruchomienie GC.

Podobnie jak w przypadku większości wytycznych i dobrych reguł praktycznych (oraz dobrych praktyk projektowych), są rzadkie sytuacje, w których warto obejść ustaloną normę. Musisz być bardzo pewien, że rozumiesz sprawę, że Twój przypadek naprawdę wymaga zniesienia powszechnej praktyki i że rozumiesz ryzyko i skutki uboczne, które możesz spowodować. Ale są takie przypadki.

Problemy programistyczne są bardzo zróżnicowane i wymagają elastycznego podejścia. Widziałem przypadki, w których ma sens blokowanie GC w językach ze śmieciami oraz w miejscach, w których ma sens, aby go wywołać, zamiast czekać, aż pojawi się naturalnie. W 95% przypadków którykolwiek z nich byłby drogowskazem, wskazującym na niewłaściwe podejście do problemu. Ale 1 raz na 20 prawdopodobnie istnieje uzasadniona argumentacja.


źródło
12

Nauczyłem się, aby nie próbować przechytrzyć zbierania śmieci. Mając to na uwadze, po prostu trzymam się usingsłowa kluczowego, gdy mam do czynienia z niezarządzanymi zasobami, takimi jak we / wy plików lub połączenia z bazami danych.

Kon
źródło
27
kompilator? co kompilator ma wspólnego z GC? :)
KristoferA
1
Nic, tylko kompiluje i optymalizuje kod. I to na pewno nie ma nic wspólnego z CLR ... a nawet .NET.
Kon
1
Obiekty zajmujące dużo pamięci, takie jak bardzo duże obrazy, mogą nie zostać zebrane jako śmieci, chyba że wyraźnie je zbierzesz. Myślę, że to (duże obiekty) było mniej więcej problemem PO.
code4life
1
Zawijanie go w użyciu zapewni, że zostanie zaplanowany dla GC, gdy nie będzie używany. Jeśli komputer nie wybuchnie, ta pamięć najprawdopodobniej zostanie wyczyszczona.
Kon
9

Nie jestem pewien, czy jest to najlepsza praktyka, ale podczas pracy z dużą ilością obrazów w pętli (tj. Tworząc i usuwając wiele obiektów Graphics / Image / Bitmap), regularnie pozwalam GC.Collect.

Myślę, że gdzieś przeczytałem, że GC działa tylko wtedy, gdy program jest (w większości) bezczynny, a nie w środku intensywnej pętli, więc może to wyglądać jak obszar, w którym ręczny GC mógłby mieć sens.

Michael Stum
źródło
Czy na pewno tego potrzebujesz? GC będą zbierać, jeśli potrzebuje pamięci, nawet jeśli kod nie jest bezczynny.
Konrad Rudolph
Nie jestem pewien, jak to jest teraz w .net 3.5 SP1, ale wcześniej (1.1 i wydaje mi się, że testowałem z 2.0) miało to wpływ na zużycie pamięci. GC będzie oczywiście zawsze zbierane w razie potrzeby, ale nadal możesz marnować 100 MB pamięci RAM, gdy potrzebujesz tylko 20. Jednak potrzebowałbyś jeszcze kilku testów
Michael Stum
2
GC jest wyzwalany przy alokacji pamięci, gdy generacja 0 osiąga pewien próg (na przykład 1 MB), a nie gdy „coś jest bezczynne”. W przeciwnym razie możesz skończyć z OutOfMemoryException w pętli, po prostu przydzielając i natychmiast odrzucając obiekty.
liggett78
5
100 mil pamięci RAM nie jest marnowane, jeśli żadne inne procesy tego nie potrzebują. To daje niezły wzrost wydajności :-P
Orion Edwards
9

Niedawno spotkałem się z przypadkiem, który wymagał ręcznego wywołania, GC.Collect()gdy pracowałem z dużymi obiektami C ++, które były opakowane w małe zarządzane obiekty C ++, do których z kolei dostęp był uzyskiwany z C #.

Moduł odśmiecania pamięci nigdy nie został wywołany, ponieważ ilość używanej pamięci zarządzanej była znikoma, ale ilość używanej pamięci niezarządzanej była ogromna. Ręczne wywoływanie Dispose()obiektów wymagałoby ode mnie śledzenia, kiedy obiekty nie są już potrzebne, podczas gdy wywołanie GC.Collect()spowoduje wyczyszczenie wszystkich obiektów, do których już nie istnieją odniesienia .....

Morten
źródło
6
Lepszym sposobem rozwiązania tego problemu jest wywołanie GC.AddMemoryPressure (ApproximateSizeOfUnmanagedResource)konstruktora, a później GC.RemoveMemoryPressure(addedSize)finalizatora. W ten sposób garbage collector będzie działał automatycznie, biorąc pod uwagę rozmiar niezarządzanych struktur, które można zebrać. stackoverflow.com/questions/1149181/…
HugoRune
Jeszcze lepszym sposobem rozwiązania problemu jest wywołanie metody Dispose (), co i tak powinieneś zrobić.
fabspro
2
Najlepszym sposobem jest użycie struktury Using. Spróbuj / Wreszcie .Dispose to kłopot
TamusJRoyce
7

Myślę, że wymieniłeś już najlepsze praktyki i NIE jest to używanie ich, chyba że NAPRAWDĘ jest to konieczne. Zdecydowanie zaleciłbym przyjrzenie się kodowi bardziej szczegółowo i użycie narzędzi do profilowania, jeśli zajdzie taka potrzeba, aby najpierw odpowiedzieć na te pytania.

  1. Czy masz w kodzie coś, co deklaruje elementy w większym zakresie niż jest to potrzebne
  2. Czy użycie pamięci jest naprawdę zbyt wysokie?
  3. Porównaj wydajność przed i po użyciu GC.Collect (), aby zobaczyć, czy to naprawdę pomaga.
Mitchel Sellers
źródło
5

Załóżmy, że Twój program nie ma wycieków pamięci, obiekty gromadzą się i nie mogą być poddane GC w Gen 0, ponieważ: 1) Są przywoływane przez długi czas, więc przejdź do Gen1 i Gen2; 2) Są to duże obiekty (> 80K), więc wejdź do LOH (Large Object Heap). A LOH nie wykonuje kompaktowania, jak w Gen0, Gen1 i Gen2.

Sprawdź licznik wydajności ".NET Memory" i widzisz, że problem 1) naprawdę nie jest problemem. Ogólnie rzecz biorąc, każde 10 GC Gen0 wyzwala 1 GC Gen1, a każde 10 GC Gen1 wyzwala 1 GC Gen2. Teoretycznie GC1 i GC2 nigdy nie mogą być poddane GC, jeśli nie ma nacisku na GC0 (jeśli użycie pamięci programu jest naprawdę podłączone). Nigdy mi się to nie przytrafiło.

W przypadku problemu 2) można sprawdzić licznik wydajności „.NET Memory”, aby sprawdzić, czy LOH nie jest nadęty. Jeśli naprawdę stanowi to problem, być może możesz utworzyć pulę dużych obiektów, jak sugeruje ten blog http://blogs.msdn.com/yunjin/archive/2004/01/27/63642.aspx .

Morgan Cheng
źródło
4

Duże obiekty są przydzielane na LOH (sterta dużych obiektów), a nie na gen. 0. Jeśli mówisz, że nie są zbierane śmieci z genem 0, masz rację. Uważam, że są zbierane tylko wtedy, gdy nastąpi pełny cykl GC (generacja 0, 1 i 2).

Biorąc to pod uwagę, uważam, że z drugiej strony GC będzie dostosowywać i gromadzić pamięć bardziej agresywnie, gdy pracujesz z dużymi obiektami, gdy ciśnienie pamięci rośnie.

Trudno powiedzieć, czy zbierać, czy nie iw jakich okolicznościach. Kiedyś robiłem GC.Collect () po usunięciu okien dialogowych / formularzy z licznymi kontrolkami itp. (Ponieważ do czasu, gdy formularz i jego kontrolki kończą się w Gen 2 z powodu tworzenia wielu instancji obiektów biznesowych / ładowania dużej ilości danych - nie oczywiście duże obiekty), ale w dłuższej perspektywie nie zauważył żadnych pozytywnych ani negatywnych skutków.

liggett78
źródło
4

Dodam, że: Wywołanie GC.Collect () (+ WaitForPendingFinalizers ()) to jedna część historii. Jak słusznie wspomnieli inni, GC.COllect () jest kolekcją niedeterministyczną i jest pozostawiona uznaniu samego GC (CLR). Nawet jeśli dodasz wywołanie do WaitForPendingFinalizers, może nie być deterministyczne. Weź kod z tego linku msdn i uruchom kod z iteracją pętli obiektów na 1 lub 2. Dowiesz się, co oznacza niedeterministyczne (ustaw punkt przerwania w destruktorze obiektu). Dokładniej, destruktor nie jest wywoływany, gdy był tylko 1 (lub 2) obiekt pokutujący przez Wait .. (). [Citation reqd.]

Jeśli Twój kod ma do czynienia z niezarządzanymi zasobami (np. Zewnętrznymi uchwytami plików), musisz zaimplementować destruktory (lub finalizatory).

Oto ciekawy przykład:

Uwaga : Jeśli już wypróbowałeś powyższy przykład z MSDN, następujący kod oczyści powietrze.

class Program
{    
    static void Main(string[] args)
        {
            SomePublisher publisher = new SomePublisher();

            for (int i = 0; i < 10; i++)
            {
                SomeSubscriber subscriber = new SomeSubscriber(publisher);
                subscriber = null;
            }

            GC.Collect();
            GC.WaitForPendingFinalizers();

            Console.WriteLine(SomeSubscriber.Count.ToString());


            Console.ReadLine();
        }
    }

    public class SomePublisher
    {
        public event EventHandler SomeEvent;
    }

    public class SomeSubscriber
    {
        public static int Count;

        public SomeSubscriber(SomePublisher publisher)
        {
            publisher.SomeEvent += new EventHandler(publisher_SomeEvent);
        }

        ~SomeSubscriber()
        {
            SomeSubscriber.Count++;
        }

        private void publisher_SomeEvent(object sender, EventArgs e)
        {
            // TODO: something
            string stub = "";
        }
    }

Sugeruję, najpierw przeanalizuj, jakie może być wyjście, a następnie uruchom, a następnie przeczytaj przyczynę poniżej:

{Destruktor jest wywoływany niejawnie dopiero po zakończeniu programu. } Aby deterministycznie wyczyścić obiekt, należy zaimplementować IDisposable i wykonać jawne wywołanie metody Dispose (). To jest istota! :)

Vaibhav
źródło
2

Jeszcze jedno, jawne uruchomienie GC Collect może NIE poprawić wydajności programu. Całkiem możliwe, że będzie gorzej.

NET GC jest dobrze zaprojektowany i dostrojony do adaptacji, co oznacza, że ​​może dostosować próg GC0 / 1/2 zgodnie z „nawykiem” wykorzystania pamięci programu. Tak więc po pewnym czasie zostanie dostosowany do twojego programu. Po jawnym wywołaniu GC.Collect progi zostaną zresetowane! A .NET musi poświęcić trochę czasu na ponowne przystosowanie się do „nawyków” programu.

Moja sugestia jest zawsze zaufana .NET GC. Pojawią się jakiekolwiek problemy z pamięcią, sprawdź licznik wydajności „.NET Memory” i zdiagnozuj mój własny kod.

Morgan Cheng
źródło
6
Myślę, że lepiej będzie, jeśli połączysz tę odpowiedź z poprzednią odpowiedzią.
Salamander 2007,
1

Nie jestem pewien, czy to najlepsza praktyka ...

Sugestia: nie wdrażaj tego ani niczego, jeśli nie masz pewności. Dokonaj ponownej oceny, gdy znane są fakty, a następnie przeprowadź testy wydajności przed / po, aby zweryfikować.

user957965
źródło
0

Jeśli jednak możesz niezawodnie przetestować swój kod, aby potwierdzić, że wywołanie Collect () nie będzie miało negatywnego wpływu, kontynuuj ...

IMHO, jest to podobne do powiedzenia „Jeśli możesz udowodnić, że Twój program nigdy nie będzie zawierał żadnych błędów w przyszłości, to śmiało ...”

Mówiąc poważnie, wymuszanie GC jest przydatne do debugowania / testowania. Jeśli czujesz, że musisz to zrobić w innym czasie, albo się mylisz, albo Twój program został źle zbudowany. Tak czy inaczej, rozwiązaniem nie jest wymuszenie GC ...

Orion Edwards
źródło
„to albo się mylisz, albo twój program został źle zbudowany. Tak czy inaczej, rozwiązaniem nie jest wymuszenie GC ...” Absoluty prawie zawsze nie są prawdziwe. To ma sens w wyjątkowych okolicznościach.
rolki