Lub innymi słowy, jakie konkretne problemy rozwiązało automatyczne usuwanie śmieci? Nigdy nie programowałem na niskim poziomie, więc nie wiem, jak skomplikowane może być uwolnienie zasobów.
Tego rodzaju błędy, które GC wydaje (przynajmniej zewnętrznemu obserwatorowi), są czymś, czego nie zrobiłby programista dobrze znający swój język, biblioteki, koncepcje, idiomy itp. Ale mogę się mylić: czy ręczne przetwarzanie pamięci jest wewnętrznie skomplikowane?
Odpowiedzi:
Zabawne, jak zmienia się z czasem definicja „niskiego poziomu”. Kiedy uczyłem się programowania, każdy język, który zapewniał znormalizowany model sterty, który umożliwia prosty wzorzec alokacji / swobodnego, był rzeczywiście uważany za wysoki poziom. W programowaniu niskiego poziomu musisz sam śledzić pamięć (nie alokacje, ale same lokalizacje pamięci!) Lub napisać własny alokator sterty, jeśli naprawdę masz ochotę.
To powiedziawszy, w ogóle nie ma w tym nic strasznego ani „skomplikowanego”. Pamiętasz, jak byłeś dzieckiem, a mama kazała ci odłożyć zabawki, kiedy skończysz z nimi bawić, że nie jest twoją pokojówką i nie zamierza dla ciebie posprzątać pokoju? Zarządzanie pamięcią to po prostu ta sama zasada stosowana w kodzie. (GC jest jak pokojówka, którzy będą sprzątać po tobie, ale ona jest bardzo leniwy i nieco pojęcia.) Zasada jest prosty: Każda zmienna w kodzie ma jednego i tylko jednego właściciela, a to w gestii tego właściciela zwolnij pamięć zmiennej, gdy nie jest już potrzebna. ( Zasada jednolitej własności) Wymaga to jednego wywołania na przydział i istnieje kilka schematów, które automatyzują własność i czyszczenie w taki czy inny sposób, więc nie musisz nawet zapisywać tego wywołania we własnym kodzie.
Odśmiecanie ma rozwiązać dwa problemy. Niezmiennie wykonuje bardzo złą pracę na jednym z nich, a w zależności od implementacji może, ale nie musi, dobrze działać na drugim. Problemy to wycieki pamięci (trzymanie się pamięci po jej zakończeniu) i wiszące odniesienia (zwalnianie pamięci, zanim skończysz.) Spójrzmy na oba problemy:
Wiszące referencje: Najpierw przedyskutuj to, bo to naprawdę poważne. Masz dwa wskaźniki do tego samego obiektu. Uwalniasz jeden z nich i nie zauważasz drugiego. Następnie w pewnym momencie spróbujesz odczytać (lub napisać lub uwolnić) drugi. Następuje niezdefiniowane zachowanie. Jeśli tego nie zauważysz, możesz łatwo uszkodzić swoją pamięć. Odśmiecanie ma uniemożliwić ten problem, zapewniając, że nic nigdy nie zostanie uwolnione, dopóki nie znikną wszystkie odniesienia do niego. W języku w pełni zarządzanym to prawie działa, dopóki nie będziesz mieć do czynienia z zewnętrznymi, niezarządzanymi zasobami pamięci. Następnie wraca do kwadratu 1. W języku niezarządzanym sprawy są jeszcze trudniejsze. (Grzebać w Mozilli '
Na szczęście poradzenie sobie z tym problemem jest w zasadzie rozwiązanym problemem. Nie potrzebujesz śmietnika, potrzebujesz debugującego menedżera pamięci. Używam na przykład Delphi, a dzięki jednej bibliotece zewnętrznej i prostej dyrektywie kompilatora mogę ustawić alokator na „Tryb pełnego debugowania”. Dodaje to nieznaczny (mniej niż 5%) narzut wydajności w zamian za włączenie niektórych funkcji, które śledzą zużycie pamięci. Jeśli uwolnię obiekt, wypełni on swoją pamięć
0x80
bajty (łatwo rozpoznawalne w debuggerze) i jeśli kiedykolwiek spróbuję wywołać metodę wirtualną (w tym destruktor) na uwolnionym obiekcie, zauważy i przerwie program z polem błędu z trzema śladami stosu - kiedy obiekt został utworzony, kiedy został uwolniony i gdzie jestem teraz - plus kilka innych przydatnych informacji, a następnie podnosi wyjątek. Nie jest to oczywiście odpowiednie dla kompilacji wersji, ale sprawia, że śledzenie i naprawianie wiszących problemów z referencjami jest banalne.Drugi problem to wycieki pamięci. Dzieje się tak, gdy nadal będziesz trzymać przydzieloną pamięć, kiedy już jej nie potrzebujesz. Może się to zdarzyć w dowolnym języku, z lub bez wyrzucania elementów bezużytecznych, i można to naprawić tylko poprzez prawidłowe wpisanie kodu. Wyrzucanie elementów bezużytecznych pomaga złagodzić jedną określoną formę wycieku pamięci, taką, która występuje, gdy nie ma prawidłowych odwołań do fragmentu pamięci, który nie został jeszcze zwolniony, co oznacza, że pamięć pozostaje przydzielona aż do zakończenia programu. Niestety jedynym sposobem na osiągnięcie tego w sposób zautomatyzowany jest przekształcenie każdej alokacji w wyciek pamięci!
Prawdopodobnie zostanę oszukany przez zwolenników GC, jeśli spróbuję powiedzieć coś takiego, więc pozwólcie mi wyjaśnić. Pamiętaj, że definicja wycieku pamięci utrzymuje przydzieloną pamięć, gdy nie jest już potrzebna. Oprócz braku odniesień do czegoś, możesz również przeciekać pamięć, posiadając niepotrzebne odniesienie, takie jak trzymanie go w obiekcie kontenerowym, gdy powinieneś go uwolnić. Widziałem pewne wycieki pamięci spowodowane przez to i bardzo trudno jest wyśledzić, czy masz GC, czy nie, ponieważ zawierają one całkowicie poprawne odniesienie do pamięci i nie ma wyraźnych „błędów” do debugowania narzędzi złapać. O ile mi wiadomo, nie ma zautomatyzowanego narzędzia, które pozwala wykryć tego rodzaju wycieki pamięci.
Więc śmieciarz zajmuje się tylko różnorodnymi wyciekami pamięci, ponieważ jest to jedyny typ, którym można zaradzić w sposób zautomatyzowany. Gdyby mógł obejrzeć wszystkie twoje odniesienia do wszystkiego i uwolnić każdy obiekt, jak tylko będzie miał do niego zero odniesień, byłoby idealnie, przynajmniej w odniesieniu do problemu braku odniesień. Robiąc to w sposób zautomatyzowany nazywa się liczeniem referencji i można to zrobić w niektórych ograniczonych sytuacjach, ale ma on swoje własne problemy. (Na przykład obiekt A z odniesieniem do obiektu B, który zawiera odniesienie do obiektu A. W schemacie zliczania odwołań żaden obiekt nie może zostać zwolniony automatycznie, nawet jeśli nie ma zewnętrznych odniesień do A lub B.) śmieciarze używają śledzeniazamiast tego: zacznij od zestawu znanych dobrych obiektów, znajdź wszystkie obiekty, do których się odwołują, znajdź wszystkie obiekty, do których się odwołują, i tak dalej, aż znajdziesz wszystko. Cokolwiek nie zostanie znalezione w procesie śledzenia, to śmieci i można je wyrzucić. (Wykonanie tego z powodzeniem wymaga oczywiście języka zarządzanego, który nakłada pewne ograniczenia na system typów, aby zapewnić, że moduł śledzenia śmieci może zawsze odróżnić odwołanie od jakiegoś losowego fragmentu pamięci, który wygląda jak wskaźnik).
Istnieją dwa problemy ze śledzeniem. Po pierwsze, jest wolny i podczas trwania programu program musi być mniej lub bardziej wstrzymany, aby uniknąć warunków wyścigu. Może to prowadzić do zauważalnych problemów z wykonywaniem, gdy program ma wchodzić w interakcje z użytkownikiem, lub obniżonej wydajności w aplikacji serwera. Można to złagodzić za pomocą różnych technik, takich jak podział przydzielonej pamięci na „generacje” na zasadzie, że jeśli przydział nie zostanie zebrany przy pierwszej próbie, prawdopodobnie pozostanie na jakiś czas. Zarówno środowisko .NET, jak i JVM korzystają z generacyjnych modułów czyszczących.
Niestety przyczynia się to do drugiego problemu: pamięć nie jest uwalniana, gdy skończysz. O ile śledzenie nie uruchomi się natychmiast po skończeniu z obiektem, pozostanie w nim do następnego śladu, a nawet dłużej, jeśli minie pierwszą generację. W rzeczywistości jedno z najlepszych wyjaśnień dotyczących modułu czyszczącego .NET, jakie widziałem, wyjaśnia, że aby proces był tak szybki, jak to możliwe, GC musi odkładać zbieranie na tak długo, jak to możliwe! Problem wycieków pamięci jest więc „rozwiązywany” dość dziwnie przez wyciekanie jak największej ilości pamięci na tak długo, jak to możliwe! To mam na myśli, gdy mówię, że GC zamienia każdą alokację w wyciek pamięci. W rzeczywistości nie ma gwarancji, że dany obiekt zostanie kiedykolwiek odebrany.
Dlaczego jest to problem, gdy pamięć jest nadal odzyskiwana w razie potrzeby? Z kilku powodów. Najpierw wyobraź sobie przydzielenie dużego obiektu (na przykład bitmapy), który zajmuje znaczną ilość pamięci. A potem, wkrótce po zakończeniu, potrzebujesz kolejnego dużego obiektu, który zajmuje tyle samo (lub blisko tej samej) ilości pamięci. Gdyby pierwszy obiekt został uwolniony, drugi może ponownie wykorzystać swoją pamięć. Ale w systemie śmieciowym być może nadal czekasz na uruchomienie następnego śladu, więc niepotrzebnie marnujesz pamięć na drugi duży obiekt. Zasadniczo jest to warunek wyścigu.
Po drugie, niepotrzebne przechowywanie pamięci, szczególnie w dużych ilościach, może powodować problemy w nowoczesnym systemie wielozadaniowym. Jeśli zajmiesz zbyt dużo pamięci fizycznej, może to spowodować, że Twój program lub inne programy będą musiały stronicować (zamieniać część pamięci na dysk), co naprawdę spowalnia działanie. W przypadku niektórych systemów, takich jak serwery, stronicowanie może nie tylko spowolnić system, ale może spowodować awarię całego systemu, jeśli jest obciążony.
Podobnie jak problem z wiszącymi referencjami, problem braku referencji można rozwiązać za pomocą debugującego menedżera pamięci. Ponownie wspomnę o trybie pełnego debugowania z menedżera pamięci FastMM firmy Delphi, ponieważ jest to ten, który znam najbardziej. (Jestem pewien, że podobne systemy istnieją dla innych języków).
Po zakończeniu działania programu działającego pod FastMM możesz opcjonalnie zgłosić istnienie wszystkich przydziałów, które nigdy nie zostały zwolnione. Tryb pełnego debugowania idzie o krok dalej: może zapisać plik na dysku zawierający nie tylko typ alokacji, ale ślad stosu od momentu przydzielenia i inne informacje debugowania dla każdej alokacji wyciekającej. To sprawia, że śledzenie wycieków pamięci bez odniesień jest banalne.
Kiedy naprawdę na to patrzysz, wyrzucanie elementów bezużytecznych może, ale nie musi, dobrze zapobiegać zwisaniu referencji i ogólnie źle radzi sobie z wyciekiem pamięci. Jego jedyną zaletą nie jest samo zbieranie śmieci, ale efekt uboczny: zapewnia zautomatyzowany sposób wykonywania zagęszczania hałdy. Może to zapobiec tajemnemu problemowi (wyczerpaniu pamięci przez fragmentację sterty), który może zabić programy działające nieprzerwanie przez długi czas i charakteryzujące się wysokim stopniem rezygnacji z pamięci, a kompresja sterty jest prawie niemożliwa bez odśmiecania. Jednak każdy dobry alokator pamięci korzysta obecnie z segmentów, aby zminimalizować fragmentację, co oznacza, że fragmentacja naprawdę staje się problemem tylko w ekstremalnych okolicznościach. W przypadku programu, w którym fragmentacja sterty może stanowić problem, „ Zalecane jest użycie kompaktowego pojemnika na śmieci. Ale IMO w każdym innym przypadku użycie odśmiecania jest przedwczesną optymalizacją i istnieją lepsze rozwiązania problemów, które „rozwiązuje”.
źródło
Biorąc pod uwagę technikę zarządzania pamięcią bez gromadzenia śmieci z epoki równoważnej, jak śmieciarki używane w obecnych popularnych systemach, takich jak RAII C ++. Biorąc pod uwagę to podejście, koszt nieużywania automatycznego usuwania śmieci jest minimalny, a GC wprowadza wiele własnych problemów. Jako taki sugerowałbym, że „Niewiele” jest odpowiedzią na twój problem.
Pamiętaj, kiedy ludzie myślą o GC, myślą
malloc
ifree
. Ale to jest gigantyczny logiczny błąd - porównujesz zarządzanie zasobami spoza GC z początku lat 70. do śmieciarek z końca lat 90. Jest to oczywiście raczej niesprawiedliwe comparison- kolektory śmieci, które były używane podczasmalloc
ifree
zostały zaprojektowane były zbyt powolne, aby uruchomić żadnego sensownego programu, jeśli dobrze pamiętam. Porównanie czegoś z niejasnego przedziału czasu, np.unique_ptr
, Jest znacznie bardziej znaczące.Śmieciarki mogą łatwiej obsługiwać cykle referencyjne, chociaż są to dość rzadkie doświadczenia. Ponadto GC mogą po prostu „wyrzucić” kod, ponieważ GC zajmie się zarządzaniem pamięcią, co oznacza, że mogą one prowadzić do szybszych cykli programistycznych.
Z drugiej strony, mają oni do czynienia z ogromnymi problemami, gdy mają do czynienia z pamięcią pochodzącą z dowolnego miejsca poza własną pulą GC. Ponadto tracą wiele korzyści, gdy w grę wchodzi współbieżność, ponieważ i tak należy rozważyć własność obiektu.
Edycja: wiele z wymienionych przez ciebie rzeczy nie ma nic wspólnego z GC. Mylisz zarządzanie pamięcią i orientację obiektową. Zobacz, o co chodzi: jeśli programujesz w całkowicie niezarządzanym systemie, takim jak C ++, możesz mieć tyle ograniczeń sprawdzania, ile chcesz, a oferują to standardowe klasy kontenerów. Na przykład nie ma nic GC w sprawdzaniu granic lub silnym pisaniu.
Wspomniane problemy są rozwiązywane przez orientację obiektową, a nie GC. Źródłem pamięci tablicowej i upewnianiem się, że nie piszesz poza nią, są pojęcia ortogonalne.
Edycja: Warto zauważyć, że bardziej zaawansowane techniki mogą w ogóle uniknąć potrzeby jakiejkolwiek formy dynamicznej alokacji pamięci. Rozważmy na przykład użycie tego , który implementuje kombinację Y w C ++ bez dynamicznej alokacji.
źródło
„Wolność od martwienia się o uwolnienie zasobów”, którą rzekomo zapewniają języki gromadzące śmieci, jest w dużej mierze iluzją. Dodawaj rzeczy do mapy, nie usuwając żadnych, a wkrótce zrozumiesz, o czym mówię.
W rzeczywistości przecieki pamięci są dość częste w programach napisanych w językach GCed, ponieważ języki te powodują, że programiści są leniwi i sprawiają, że zyskują fałszywe poczucie bezpieczeństwa, że język zawsze (magicznie) zajmie się każdym obiektem, który nie chcę już o tym myśleć.
Odśmiecanie jest po prostu niezbędnym ułatwieniem dla języków, które mają inny, bardziej szlachetny cel: traktować wszystko jako wskaźnik do obiektu, a jednocześnie ukrywać przed programistą fakt, że jest to wskaźnik, aby programista nie mógł zatwierdzić samobójstwo poprzez próbę arytmetyki wskaźnika i tym podobne. Wszystko, co jest przedmiotem, oznacza, że języki GCed muszą przydzielać obiekty znacznie częściej niż języki inne niż GCed, co oznacza, że gdyby nałożyły ciężar zwolnienia tych obiektów na programistę, byłyby niezwykle nieatrakcyjne.
Ponadto odśmiecanie jest przydatne, aby zapewnić programiście możliwość pisania ścisłego kodu, manipulowania obiektami w wyrażeniach, w funkcjonalny sposób programowania, bez konieczności dzielenia wyrażeń na osobne instrukcje, aby umożliwić dealokację każdego pojedynczy obiekt, który uczestniczy w wyrażeniu.
Poza tym, proszę zauważyć, że na początku mojej odpowiedzi napisałem „jest to w dużej mierze iluzja”. Nie napisałem, że to złudzenie. Nawet nie napisałem, że to głównie złudzenie. Odśmiecanie jest przydatne w odejmowaniu od programisty podrzędnego zadania polegającego na zwalnianiu jego obiektów. W tym sensie jest to funkcja produktywności.
źródło
Garbage collector nie usuwa żadnych „błędów”. Jest to niezbędna część semantyki języków wysokiego poziomu. Za pomocą GC można zdefiniować wyższe poziomy abstrakcji, takie jak zamknięcia leksykalne i tym podobne, natomiast przy ręcznym zarządzaniu pamięcią abstrakcje te będą nieszczelne, niepotrzebnie związane z niższymi poziomami zarządzania zasobami.
„Zasada jednolitej własności”, o której mowa w komentarzach, jest dość dobrym przykładem takiej nieszczelnej abstrakcji. Deweloper nie powinien przejmować się liczbą linków do żadnej konkretnej podstawowej struktury danych, w przeciwnym razie żaden fragment kodu nie byłby ogólny i przejrzysty bez ogromnej liczby dodatkowych (niewidocznych bezpośrednio w samym kodzie) ograniczeń i wymagań . Taki kod nie może zostać złożony w kod wyższego poziomu, co stanowi niedopuszczalne naruszenie zasady podziału odpowiedzialności (główny element inżynierii oprogramowania, niestety w ogóle nie przestrzegany przez większość programistów niskiego poziomu).
źródło
Naprawdę, zarządzanie własną pamięcią to jeszcze jedno potencjalne źródło błędów.
Jeśli zapomnisz wywołania
free
(lub innego odpowiednika w dowolnym języku, którego używasz), twój program może przejść wszystkie testy, ale pamięć wycieku. W średnio złożonym programie dość łatwo przeoczyć połączenie zfree
.źródło
free
nie jest najgorsze. Wczesnefree
jest o wiele bardziej niszczycielskie.free
!malloc
ifree
były drogami spoza GC, były zdecydowanie zbyt wolne, aby były przydatne do czegokolwiek. Musisz porównać to z nowoczesnym podejściem innym niż GC, takim jak RAII.Zasoby ręczne są nie tylko uciążliwe, ale również trudne do debugowania. Innymi słowy, nie tylko żmudne jest poprawne zrobienie tego, ale także, gdy się pomylisz, nie jest oczywiste, gdzie jest problem. Wynika to z faktu, że w przeciwieństwie do na przykład dzielenia przez zero, skutki błędu pokazują się z dala od źródła błędu, a łączenie kropek wymaga czasu, uwagi i doświadczenia.
źródło
Wydaje mi się, że zbieranie śmieci ma duże uznanie za ulepszenia języka, które nie mają nic wspólnego z GC, poza byciem częścią jednej wielkiej fali postępu.
Jedyną solidną korzyścią dla GC, o której wiem, jest to, że możesz uwolnić obiekt w swoim programie i wiedzieć, że zniknie, gdy wszyscy skończą. Możesz przekazać to do metody innej klasy i nie martw się o to. Nie obchodzi Cię, jakie inne metody są przekazywane ani do jakich klas to odwołują. (Wycieki pamięci są obowiązkiem klasy odwołującej się do obiektu, a nie klasy, która go utworzyła).
Bez GC musisz śledzić cały cykl życia przydzielonej pamięci. Za każdym razem, gdy przekazujesz adres w górę lub w dół z podprogramu, który go utworzył, masz niekontrolowane odwołanie do tej pamięci. W dawnych, złych czasach, nawet z jednym wątkiem, rekurencja i ornery system operacyjny (Windows NT) uniemożliwiły mi kontrolowanie dostępu do przydzielonej pamięci. Musiałem sfałszować darmową metodę w moim własnym systemie alokacji, aby trzymać bloki pamięci przez pewien czas, aż wszystkie referencje zostaną usunięte. Czas oczekiwania był czysty, ale działał.
To jedyna znana mi korzyść z GC, ale bez niej nie mogłabym żyć. Nie sądzę, żeby jakikolwiek OOP poleciałby bez niego.
źródło
Wycieki fizyczne
Pochodząc z końca C, co sprawia, że zarządzanie pamięcią jest tak ręczne i wyraźne, jak to możliwe, dzięki czemu porównujemy skrajności (C ++ głównie automatyzuje zarządzanie pamięcią bez GC), powiedziałbym „nie bardzo” w sensie porównywania z GC, kiedy to dochodzi do wycieków . Początkujący, a czasem nawet zawodowiec może zapomnieć o napisaniu
free
na dany tematmalloc
. Zdecydowanie tak się dzieje.Istnieją jednak takie narzędzia, jak
valgrind
wykrywanie wycieków, które natychmiast wykryją podczas wykonywania kodu, kiedy / gdzie takie błędy wystąpią aż do dokładnej linii kodu. Po zintegrowaniu z CI, łączenie takich błędów staje się prawie niemożliwe, a ich poprawianie jest łatwe. Więc nigdy nie jest to wielka sprawa w żadnym zespole / procesie z rozsądnymi standardami.To prawda, że mogą wystąpić egzotyczne przypadki wykonywania, które latają pod radarem testowania, gdzie
free
nie zostały wywołane, być może w przypadku napotkania niejasnego zewnętrznego błędu wejściowego, takiego jak uszkodzony plik, w którym to przypadku system może przeciekać 32 bajty lub coś takiego. Myślę, że na pewno może się to zdarzyć nawet przy całkiem dobrych standardach testowania i narzędziach do wykrywania wycieków, ale przecież wyciek odrobiny pamięci na coś, co prawie nigdy się nie zdarzy, nie byłoby tak istotne. Zobaczymy znacznie większy problem, w którym możemy wyciec ogromne zasoby, nawet w typowych ścieżkach wykonania poniżej, w sposób, którego GC nie może zapobiec.Jest to również trudne bez czegoś przypominającego pseudo-formę GC (liczenie referencji, np.), Gdy czas życia obiektu musi zostać przedłużony dla jakiejś formy odroczonego / asynchronicznego przetwarzania, być może o inny wątek.
Zwisające wskaźniki
Prawdziwy problem z bardziej ręcznymi formami zarządzania pamięcią nie jest dla mnie wyciekiem. Ile znanych aplikacji napisanych w C lub C ++ jest naprawdę nieszczelnych? Czy jądro Linuksa jest nieszczelne? MySQL? CryEngine 3? Cyfrowe stacje robocze i syntezatory audio? Czy Java VM wyciek (jest zaimplementowany w kodzie natywnym)? Photoshop?
Jeśli już, myślę, że kiedy się rozejrzymy, najbardziej nieszczelnymi aplikacjami są te napisane przy użyciu schematów GC. Ale zanim zostanie to potraktowane jako trzask podczas wyrzucania elementów bezużytecznych, w natywnym kodzie występuje znaczący problem, który w ogóle nie jest związany z wyciekami pamięci.
Sprawą dla mnie zawsze było bezpieczeństwo. Nawet gdy
free
zapamiętujemy wskaźnik, jeśli istnieją inne wskaźniki do zasobu, staną się wiszącymi (unieważnionymi) wskaźnikami.Kiedy próbujemy uzyskać dostęp do punktów tych zwisających wskaźników, w końcu spotykamy się z nieokreślonym zachowaniem, chociaż prawie zawsze segfault / naruszenie dostępu prowadzące do ciężkiej, natychmiastowej awarii.
Wszystkie natywne aplikacje, które wymieniłem powyżej, potencjalnie mają niejasną obudowę lub dwie, które mogą prowadzić do awarii głównie z powodu tego problemu, i zdecydowanie jest spora część tandetnych aplikacji napisanych w natywnym kodzie, które są bardzo obciążone awarią i często w dużej mierze z powodu tego problemu.
... a to dlatego, że zarządzanie zasobami jest trudne niezależnie od tego, czy używasz GC, czy nie. Praktyczną różnicą jest często wyciek (GC) lub awaria (bez GC) w obliczu błędu prowadzącego do niewłaściwego zarządzania zasobami.
Zarządzanie zasobami: Odśmiecanie
Złożone zarządzanie zasobami jest trudnym, ręcznym procesem bez względu na wszystko. GC nie może tu nic zautomatyzować.
Weźmy przykład, w którym mamy ten obiekt „Joe”. Joe jest wymieniany przez wiele organizacji, których jest członkiem. Co około miesiąc pobierają opłatę członkowską z jego karty kredytowej.
Mamy też jedno odniesienie do Joe, który kontroluje jego życie. Powiedzmy, że jako programiści nie potrzebujemy już Joe. Zaczyna nas męczyć i nie potrzebujemy już organizacji, do których on należy, aby tracić czas na zajmowanie się nim. Próbujemy więc zetrzeć go z powierzchni ziemi, usuwając odniesienie do jego linii życia.
... ale czekaj, używamy śmiecia. Każde silne odniesienie do Joe utrzyma go przy sobie. Usuwamy więc również odniesienia do niego z organizacji, do których należy (rezygnując z subskrypcji).
... poza tym, niestety, zapomnieliśmy anulować jego subskrypcję magazynu! Teraz Joe pozostaje w pamięci, nęka nas i zużywa zasoby, a firma magazynowa również kończy proces członkostwa Joe co miesiąc.
Jest to główny błąd, który może spowodować wyciek wielu złożonych programów napisanych przy użyciu schematów wyrzucania elementów bezużytecznych i rozpoczęcie korzystania z coraz większej ilości pamięci, im dłużej działają, i być może coraz większe przetwarzanie (cykliczna subskrypcja magazynu). Zapomnieli usunąć jedno lub więcej z tych odniesień, uniemożliwiając śmieciarzowi wykonanie jego magii, dopóki cały program nie zostanie zamknięty.
Program nie ulega jednak awarii. Jest całkowicie bezpieczny. To po prostu będzie nadal gromadzić wspomnienia, a Joe nadal będzie trwał. W przypadku wielu aplikacji tego rodzaju nieszczelne zachowanie, polegające na tym, że po prostu rzucamy coraz więcej pamięci / przetwarzania na problem, może być znacznie lepsze niż awaria, szczególnie biorąc pod uwagę, ile pamięci i mocy obliczeniowej mają dziś nasze maszyny.
Zarządzanie zasobami: Ręcznie
Rozważmy teraz alternatywę, w której używamy wskaźników do Joe i ręcznego zarządzania pamięcią, takich jak:
Te niebieskie linki nie zarządzają życiem Joe. Jeśli chcemy go usunąć z powierzchni ziemi, ręcznie prosimy o jego zniszczenie, w ten sposób:
To normalnie pozostawiłoby nas z wiszącymi wskaźnikami w dowolnym miejscu, więc usuńmy wskaźniki do Joe.
... ups, znowu popełniamy ten sam błąd i zapomnieliśmy wypisać się z subskrypcji magazynu Joe!
Tyle że teraz mamy wiszący wskaźnik. Gdy subskrypcja magazynu próbuje przetworzyć miesięczną opłatę Joe, cały świat eksploduje - zazwyczaj natychmiast dochodzi do katastrofy.
Ten sam błąd dotyczący błędnego zarządzania zasobami, w którym programista zapomniał ręcznie usunąć wszystkie wskaźniki / odniesienia do zasobu, może prowadzić do wielu awarii w aplikacjach natywnych. Nie gromadzą pamięci, im dłużej działają, ponieważ zwykle w tym przypadku często ulegają awarii.
Prawdziwy świat
Teraz powyższy przykład wykorzystuje absurdalnie prosty schemat. Aplikacja w świecie rzeczywistym może wymagać połączenia tysięcy zdjęć w celu pokrycia pełnego wykresu, z setkami różnych rodzajów zasobów przechowywanych na wykresie sceny, zasobów GPU powiązanych z niektórymi z nich, akceleratorami powiązanymi z innymi, obserwatorami rozmieszczonymi w setkach wtyczek obserwowanie na scenie wielu typów bytów, obserwatorów obserwujących obserwatorów, audio zsynchronizowanych z animacjami itp. Może więc wydawać się, że łatwo jest uniknąć błędu, który opisałem powyżej, ale w rzeczywistości nie jest to takie proste w świecie rzeczywistym produkcyjna baza kodu dla złożonej aplikacji obejmującej miliony linii kodu.
Szansa, że ktoś kiedyś źle zarządza zasobami gdzieś w tej bazie kodu, jest zwykle dość wysoka, a prawdopodobieństwo jest takie samo z GC lub bez. Główną różnicą jest to, co stanie się w wyniku tego błędu, co również wpływa potencjalnie na szybkość wykrycia i naprawienia tego błędu.
Crash vs. Leak
Który z nich jest gorszy? Natychmiastowa awaria, czy cichy wyciek pamięci, w którym Joe po prostu tajemniczo zostaje?
Większość może odpowiedzieć na to drugie, ale powiedzmy, że to oprogramowanie jest zaprojektowane do działania przez wiele godzin, być może dni, a każde z tych dodanych przez nas Joe i Jane zwiększa wykorzystanie pamięci przez gigabajt. To nie jest oprogramowanie o krytycznym znaczeniu (awarie nie zabijają użytkowników), ale oprogramowanie o krytycznym znaczeniu.
W takim przypadku twarda awaria, która natychmiast pojawia się podczas debugowania, wskazując popełniony błąd, może być lepsza niż tylko nieszczelne oprogramowanie, które może nawet przelecieć pod radarem twojej procedury testowej.
Z drugiej strony, jeśli jest to oprogramowanie o kluczowym znaczeniu dla misji, w którym wydajność nie jest celem, po prostu nie ulega awarii w jakikolwiek możliwy sposób, wówczas wyciek może być w rzeczywistości lepszy.
Słabe referencje
Istnieje rodzaj hybrydy tych pomysłów dostępnych w schematach GC znanych jako słabe referencje. Przy słabych referencjach możemy sprawić, że wszystkie te organizacje będą miały słabe referencje Joe, ale nie zapobiegniemy usunięciu go, gdy silne referencje (właściciel / linia życia Joe) znikną. Niemniej jednak mamy tę zaletę, że jesteśmy w stanie wykryć, kiedy Joe nie jest już w pobliżu dzięki tym słabym referencjom, co pozwala nam uzyskać łatwo powtarzalny rodzaj błędu.
Niestety, słabe referencje nie są używane tak często, jak powinny, więc często wiele złożonych aplikacji GC może być podatnych na wycieki, nawet jeśli są one potencjalnie znacznie mniej awaryjne niż złożone aplikacje C, np.
W każdym razie to, czy GC ułatwi ci życie, zależy od tego, jak ważne jest, aby twoje oprogramowanie unikało wycieków i czy zajmuje się złożonym zarządzaniem tego rodzaju zasobami.
W moim przypadku pracuję w dziedzinie krytycznej pod względem wydajności, w której zasoby zajmują setki megabajtów do gigabajtów, i nie zwalnianie tej pamięci, gdy użytkownicy żądają zwolnienia z powodu błędu takiego jak powyższy, może być mniej preferowane niż awaria. Awarie są łatwe do wykrycia i odtworzenia, co czyni je często ulubionym rodzajem błędu programisty, nawet jeśli jest to najmniej ulubiony użytkownika, a wiele z tych awarii pojawi się wraz z rozsądną procedurą testową, zanim dotrze do użytkownika.
W każdym razie są to różnice między GC a ręcznym zarządzaniem pamięcią. Aby odpowiedzieć na twoje bezpośrednie pytanie, powiedziałbym, że ręczne zarządzanie pamięcią jest trudne, ale ma bardzo niewiele wspólnego z przeciekami, a zarówno GC, jak i ręczne formy zarządzania pamięcią są nadal bardzo trudne, gdy zarządzanie zasobami nie jest trywialne. GC ma zapewne trudniejsze zachowanie tutaj, gdzie program wydaje się działać dobrze, ale zużywa coraz więcej zasobów. Formularz ręczny jest mniej skomplikowany, ale będzie się zawieszał i spłonął dużą ilością błędów, takich jak pokazany powyżej.
źródło
Oto lista problemów, przed którymi stają programiści C ++ podczas pracy z pamięcią:
Jak widać, pamięć sterty rozwiązuje wiele istniejących problemów, ale powoduje dodatkową złożoność. GC jest zaprojektowany do obsługi części tej złożoności. (przepraszam, jeśli niektóre nazwy problemów nie są poprawne dla tych problemów - czasami trudno jest znaleźć prawidłową nazwę)
źródło