Jakie są złożoności programowania niezarządzanego pamięcią?


24

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?


3
Rozwiń, aby powiedzieć nam, w jaki sposób odpowiedź na twoje pytanie nie jest dostępna w Wikipedii na temat kolekcji ubrań, a dokładniej w sekcji o jej zaletach
yannis,

Kolejną korzyścią jest bezpieczeństwo, np. Przepełnienia bufora są wysoce podatne na wykorzystanie, a wiele innych luk w zabezpieczeniach wynika z (błędnego) zarządzania pamięcią.
StuperUser

7
@StuperUser: To nie ma nic wspólnego z początkiem pamięci. Możesz buforować pamięć przekroczenia, która pochodzi z GC. Fakt, że języki GC zwykle temu zapobiegają, jest ortogonalny, a języki, które są mniej niż trzydzieści lat za technologią GC, porównujesz je w celu zapewnienia ochrony przed przepełnieniem bufora.
DeadMG,

Odpowiedzi:


29

Nigdy nie programowałem na niskim poziomie, więc nie wiem, jak skomplikowane może być uwolnienie zasobów.

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ęć0x80bajty (ł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”.


5
Uwielbiam tę odpowiedź - czytam ją od czasu do czasu. Nie mogę wymyślić odpowiedniej uwagi, więc wszystko, co mogę powiedzieć, to - dziękuję.
vemv

3
Chciałbym zaznaczyć, że tak, GC mają tendencję do „wycieku” pamięci (przynajmniej przez jakiś czas), ale nie stanowi to problemu, ponieważ będzie zbierał pamięć, gdy program przydzielający pamięć nie będzie mógł przydzielić pamięci przed jej pobraniem. W języku innym niż GC wyciek zawsze pozostaje wyciek, co oznacza, że ​​faktycznie możesz skończyć pamięć z powodu zbyt dużej ilości niepobranej pamięci. „wyrzucanie elementów bezużytecznych to przedwczesna optymalizacja” ... GC nie jest optymalizacją i nie został zaprojektowany w tym celu. W przeciwnym razie dobra odpowiedź.
Thomas Eding,

7
@ThomasEding: GC z pewnością jest optymalizacją; optymalizuje przy minimalnym wysiłku programisty, kosztem wydajności i różnych innych wskaźników jakości programu.
Mason Wheeler

5
Zabawne, że w pewnym momencie wskazujesz narzędzie do śledzenia błędów Mozilli, ponieważ Mozilla doszła do zupełnie innych wniosków. Firefox miał i nadal ma niezliczone problemy bezpieczeństwa wynikające z błędów zarządzania pamięcią. Pamiętaj, że nie chodzi o to, jak łatwo było naprawić błąd po wykryciu --- zwykle szkoda jest już wyrządzona, zanim programiści dowiedzą się o tym problemie. Mozilla finansuje język programowania Rust właśnie po to, aby zapobiegać takim błędom.

1
Rust nie korzysta jednak z wyrzucania elementów bezużytecznych, korzysta z liczenia referencji dokładnie tak, jak opisuje Mason, tylko z obszernymi kontrolami w czasie kompilacji, zamiast używać debuggera do wykrywania błędów w czasie wykonywania ...
Sean Burton

13

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ą malloci free. 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 podczas malloci freezostał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.


Rozbudowana tutaj dyskusja została oczyszczona: jeśli każdy może zabrać ją na czat, aby omówić ten temat dalej, naprawdę byłbym wdzięczny.

@DeadMG, czy wiesz, co powinien robić kombinator? Ma się łączyć. Z definicji kombinator jest funkcją bez żadnych wolnych zmiennych.
SK-logic

2
@ SK-logic: Mogłem zdecydować się na implementację go wyłącznie według szablonu i nie mieć żadnych zmiennych składowych. Ale wtedy nie byłbyś w stanie przejść w zamknięciach, co znacznie ogranicza jego przydatność. Chcesz przyjść na czat?
DeadMG,

@DeadMG, definicja jest krystalicznie czysta. Brak wolnych zmiennych. Uważam każdy język za „wystarczająco funkcjonalny”, jeśli można zdefiniować kombinator Y (właściwie, nie na twój sposób). Duże „+” oznacza, czy można je zdefiniować za pomocą kombinacji S, K i I. W przeciwnym razie język nie jest wystarczająco wyrazisty.
SK-logic

4
@ SK-logic: Dlaczego nie przyjdziesz na czat , jak poprosił miły moderator? Kombinator Y jest także kombinatorem Y, spełnia swoje zadanie lub nie. Wersja H-kombinatora kombinatora Y jest w zasadzie dokładnie taka sama jak ta, po prostu ukryty przed tobą stan wyrażony.
DeadMG,

11

„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.


4

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).


1
@Mason Wheeler, nawet C ++ implementuje bardzo ograniczoną formę zamknięć. Ale nie jest to prawie właściwe, ogólnie przydatne zamknięcie.
SK-logic

1
Jesteś w błędzie. Żaden GC nie może cię ochronić przed faktem, że nie możesz odwoływać się do zmiennych stosu. I to jest zabawne - w C ++ możesz również zastosować podejście „Kopiuj wskaźnik do zmiennej dynamicznie przydzielanej, która zostanie odpowiednio i automatycznie zniszczona”.
DeadMG,

1
@DeadMG, czy nie widzisz, że twój kod przecieka jednostki niskiego poziomu przez jakikolwiek inny poziom, który budujesz na górze?
SK-logic

1
@ SK-Logic: OK, mamy problem z terminologią. Jaka jest twoja definicja „prawdziwego zamknięcia” i co mogą zrobić, czego nie mogą zrobić zamknięcia Delphi? (A włączenie definicji do zarządzania pamięcią powoduje przesunięcie słupków celu. Porozmawiajmy o zachowaniu, a nie szczegółach implementacyjnych.)
Mason Wheeler

1
@ SK-Logic: ... i czy masz przykład czegoś, co można zrobić za pomocą prostych zamknięć typu lambda, których zamknięcie Delphi nie jest w stanie osiągnąć?
Mason Wheeler,

2

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 z free.


3
Nieodebrane freenie jest najgorsze. Wczesne freejest o wiele bardziej niszczycielskie.
herby

2
I podwójne free!
quant_dev

Hehe! Zgodziłbym się z obydwoma powyższymi komentarzami. Nigdy nie popełniłem żadnego z tych przestępstw (o ile wiem), ale widzę, jak straszne mogą być te skutki. Odpowiedź z quant_dev mówi wszystko - błędy przy alokacji pamięci i alokacji są niezwykle trudne do znalezienia i naprawienia.
Dawood mówi, że przywróć Monikę

1
To jest błąd. Porównujesz „wczesny 1970” do „późnego 1990”. GC, które istniały w tamtym czasie malloci freebył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.
DeadMG,

2
@DeadMG RAII nie jest ręcznym zarządzaniem pamięcią
quant_dev

2

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.


1

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.


1
Zaraz po mojej głowie, zarówno Delphi, jak i C ++ odniosły duży sukces jako języki OOP bez GC. Wszystko, czego potrzebujesz, aby zapobiec „niekontrolowanym referencjom”, to odrobina dyscypliny. Jeśli rozumiesz zasadę jednoosobowej własności (patrz moja odpowiedź), problemy, o których tu mówisz, stają się całkowitymi problemami.
Mason Wheeler,

@MasonWheeler: Kiedy nadszedł czas na uwolnienie obiektu właściciela, musi on znać wszystkie miejsca, do których odwołują się jego obiekty. Utrzymywanie tych informacji i wykorzystywanie ich do usuwania odniesień wydaje mi się strasznie dużo pracy. Często stwierdziłem, że referencje nie zostały jeszcze do końca usunięte. Musiałem oznaczyć właściciela jako usuniętego, a następnie okresowo przywracać go do życia, aby sprawdzić, czy może się bezpiecznie uwolnić. Nigdy nie korzystałem z Delphi, ale dla niewielkiego poświęcenia wydajności wykonywania C # / Java dało mi znaczny wzrost czasu programowania w stosunku do C ++. (Nie wszystkie z powodu GC, ale pomogły.)
RalphChapin

1

Wycieki fizyczne

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?

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 freena dany temat malloc. Zdecydowanie tak się dzieje.

Istnieją jednak takie narzędzia, jak valgrindwykrywanie 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 freenie 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 freezapamię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.

wprowadź opis zdjęcia tutaj

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.

wprowadź opis zdjęcia tutaj

... 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).

wprowadź opis zdjęcia tutaj

... 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:

wprowadź opis zdjęcia tutaj

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:

wprowadź opis zdjęcia tutaj

To normalnie pozostawiłoby nas z wiszącymi wskaźnikami w dowolnym miejscu, więc usuńmy wskaźniki do Joe.

wprowadź opis zdjęcia tutaj

... 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.


-1

Oto lista problemów, przed którymi stają programiści C ++ podczas pracy z pamięcią:

  1. Problem dotyczący zakresu występuje w pamięci przydzielonej do stosu: jego żywotność nie wykracza poza funkcję, w której został przydzielony. Istnieją trzy główne rozwiązania tego problemu: pamięć sterty i przesunięcie punktu alokacji w górę w stosie wywołań lub przydzielanie z obiektów wewnętrznych .
  2. Problem z rozmiarem dotyczy alokacji stosu i alokacji z wnętrza obiektu i częściowo alokowanej pamięci: Rozmiar bloku pamięci nie może się zmienić w środowisku wykonawczym. Rozwiązaniami są tablice pamięci sterty, wskaźniki, biblioteki i kontenery.
  3. Problem z kolejnością definicji występuje podczas przydzielania z obiektów wewnętrznych: klasy wewnątrz programu muszą być w odpowiedniej kolejności. Rozwiązania ograniczają zależności do drzewa i zmieniają kolejność klas i nie używają deklaracji przesyłania dalej, a także wskaźników i pamięci sterty oraz używają deklaracji przesyłania.
  4. Problem wewnątrz i na zewnątrz dotyczy pamięci przydzielonej obiektowo. Dostęp do pamięci wewnątrz obiektów jest podzielony na dwie części, część pamięci znajduje się wewnątrz obiektu, a inna pamięć poza nim, a programiści muszą poprawnie wybrać kompozycję lub odwołania na podstawie tej decyzji. Rozwiązania podejmują decyzję poprawnie lub wskaźniki i pamięć sterty.
  5. Problem z obiektami rekurencyjnymi dotyczy pamięci przydzielonej do obiektów. Rozmiar obiektów staje się nieskończony, jeśli ten sam obiekt zostanie umieszczony w sobie, a rozwiązania to odniesienia, pamięć sterty i wskaźniki.
  6. Problem śledzenia własności występuje w pamięci przydzielonej sterty, wskaźnik zawierający adres pamięci przydzielonej sterty musi zostać przekazany z punktu alokacji do punktu dezalokacji. Rozwiązania to pamięć alokowana na stosie, pamięć alokowana obiektowo, auto_ptr, shared_ptr, unique_ptr, stdlib pojemniki.
  7. Problem duplikacji własności dotyczy pamięci przydzielonej do sterty: dezalokacji można dokonać tylko raz. Rozwiązania to pamięć alokowana stosowo, pamięć alokowana obiektowo, auto_ptr, shared_ptr, unikalny_ptr, kontenery stdlib.
  8. Problem ze wskaźnikiem zerowym występuje w pamięci przydzielonej do stosu: wskaźniki mogą mieć wartość NULL, co powoduje awarię wielu operacji w czasie wykonywania. Rozwiązaniami są pamięć stosu, pamięć alokowana obiektowo oraz staranna analiza obszarów sterty i referencji.
  9. Problem wycieku pamięci dotyczy pamięci przydzielonej do stosu: zapomnienie o wywołaniu operacji usuwania dla każdego przydzielonego bloku pamięci. Rozwiązania to narzędzia takie jak valgrind.
  10. Problem przepełnienia stosu dotyczy wywołań funkcji rekurencyjnych, które używają pamięci stosu. Zwykle rozmiar stosu jest całkowicie określany w czasie kompilacji, z wyjątkiem przypadku algorytmów rekurencyjnych. Niepoprawne zdefiniowanie rozmiaru stosu systemu operacyjnego również często powoduje ten problem, ponieważ nie ma możliwości zmierzenia wymaganego rozmiaru miejsca na stosie.

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ę)


1
-1: Brak odpowiedzi na pytanie.
Sjoerd
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.