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 free
na dany temat malloc
. 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.