Jeśli muszę użyć fragmentu pamięci przez cały okres użytkowania mojego programu, czy naprawdę trzeba go zwolnić tuż przed zakończeniem programu?

67

W wielu książkach i samouczkach słyszałem, jak zaakcentowano praktykę zarządzania pamięcią i czułem, że wydarzyłyby się jakieś tajemnicze i okropne rzeczy, gdybym nie zwolnił pamięci po jej użyciu.

Nie mogę mówić za innymi systemami (chociaż dla mnie rozsądnie jest założyć, że stosują one podobną praktykę), ale przynajmniej w systemie Windows jądro ma zasadniczo gwarancję wyczyszczenia większości zasobów (z wyjątkiem kilku nieparzystych) używanych przez program po zakończeniu programu. Który obejmuje między innymi pamięć sterty.

Rozumiem, dlaczego chcesz zamknąć plik po zakończeniu korzystania z niego, aby udostępnić go użytkownikowi lub dlaczego chcesz odłączyć gniazdo podłączone do serwera w celu zaoszczędzenia przepustowości, ale wydaje się głupie muszę zarządzać Całą pamięcią używaną przez program.

Teraz zgadzam się, że to pytanie jest szerokie, ponieważ sposób, w jaki powinieneś obchodzić się z pamięcią, zależy od tego, ile pamięci potrzebujesz i kiedy jej potrzebujesz, więc zawęzię zakres tego pytania do tego: jeśli potrzebuję użyć pamięć przez cały okres użytkowania mojego programu, czy naprawdę konieczne jest zwolnienie go tuż przed zakończeniem programu?

Edycja: Pytanie zasugerowane jako duplikat dotyczyło rodziny systemów operacyjnych Unix. Jego najwyższa odpowiedź określa nawet narzędzie specyficzne dla Linuksa (np. Valgrind). To pytanie ma na celu objąć większość „normalnych” niewbudowanych systemów operacyjnych i dlaczego jest lub nie jest dobrą praktyką zwalnianie pamięci, która jest potrzebna przez cały okres życia programu.

Oczywista oczywistość
źródło
19
Oznaczono ten język jako agnostyczny, ale w wielu językach (np. Java) wcale nie jest konieczne ręczne zwalnianie pamięci; dzieje się to automatycznie jakiś czas po tym, jak ostatnie odniesienie do obiektu wykracza poza zakres
Richard Tingle
3
i oczywiście możesz pisać C ++ bez żadnego pojedynczego usuwania i byłoby dobrze dla pamięci
Nikko
5
@RichardTingle Chociaż nie mogę myśleć o żadnym innym języku poza C, a może C ++, znacznik niezależny od języka miał obejmować wszystkie języki, które nie mają wbudowanych narzędzi do wyrzucania elementów bezużytecznych. W dzisiejszych czasach jest to jednak rzadkość. Możesz argumentować, że możesz teraz zaimplementować taki system w C ++, ale nadal ostatecznie masz wybór, aby nie usunąć fragmentu pamięci.
CaptainObvious
4
Piszesz: „Jądro zasadniczo gwarantuje oczyszczenie wszystkich zasobów [...] po zakończeniu programu”. Jest to ogólnie nieprawda, ponieważ nie wszystko jest pamięcią, uchwytem lub obiektem jądra. Ale dotyczy to pamięci, do której natychmiast ograniczasz swoje pytanie.
Eric Towers

Odpowiedzi:

108

Jeśli muszę użyć fragmentu pamięci przez cały okres użytkowania mojego programu, czy naprawdę trzeba go zwolnić tuż przed zakończeniem programu?

Nie jest to obowiązkowe, ale może mieć zalety (jak również pewne wady).

Jeśli program przydzieli pamięć raz podczas jej wykonywania, a w przeciwnym razie nigdy nie zwolni jej do czasu zakończenia procesu, rozsądnym rozwiązaniem może być nie zwalnianie pamięci ręcznie i poleganie na systemie operacyjnym. W każdym nowoczesnym systemie operacyjnym, jaki znam, jest to bezpieczne, pod koniec procesu cała przydzielona pamięć jest niezawodnie zwracana do systemu.
W niektórych przypadkach nieoczyszczanie przydzielonej pamięci w sposób jawny może być nawet znacznie szybsze niż czyszczenie.

Jednak poprzez jawne zwolnienie całej pamięci pod koniec wykonywania,

  • podczas debugowania / testowania narzędzia do wykrywania wycieków pamięci nie będą wyświetlać „fałszywych alarmów”
  • może być znacznie łatwiej przenieść kod, który wykorzystuje pamięć wraz z alokacją i dezalokacją, do osobnego komponentu i użyć go później w innym kontekście, w którym czas użycia pamięci musi być kontrolowany przez użytkownika komponentu

Żywotność programów może ulec zmianie. Być może twój program jest dzisiaj małym narzędziem wiersza poleceń, którego typowy czas życia wynosi mniej niż 10 minut, i przydziela pamięć w częściach około KB co 10 sekund - więc nie trzeba w ogóle zwalniać przydzielonej pamięci przed zakończeniem programu. Później program został zmieniony i zyskuje przedłużone użycie w ramach procesu serwera trwającego kilka tygodni - więc nie zwalnianie nieużywanej pamięci pomiędzy nie jest już opcją, w przeciwnym razie program zacznie zużywać całą dostępną pamięć serwera . Oznacza to, że będziesz musiał przejrzeć cały program, a następnie dodać kod zwolnienia. Jeśli masz szczęście, jest to łatwe zadanie, jeśli nie, może być tak trudne, że szanse są duże, że przegapisz miejsce. A kiedy będziesz w takiej sytuacji, żałujesz, że nie dodałeś „darmowego”

Mówiąc bardziej ogólnie, pisanie kodu przydzielającego i związanego z nim zwolnienia przydziału zawsze jest liczone jako „dobry nawyk” wśród wielu programistów: robiąc to zawsze, zmniejszasz prawdopodobieństwo zapomnienia kodu zwolnienia w sytuacjach, w których pamięć musi zostać zwolniona.

Doktor Brown
źródło
12
Dobra uwaga na temat rozwijania dobrych nawyków programistycznych.
Lawrence,
4
Zasadniczo zdecydowanie zgadzam się z tą radą. Utrzymywanie dobrych nawyków z pamięcią jest bardzo ważne. Myślę jednak, że są wyjątki. Na przykład, jeśli przypisałeś jakiś zwariowany wykres, którego przejście zajmie kilka sekund i uwolnienie „poprawnie”, wtedy poprawisz wrażenia użytkownika, kończąc program i pozwalając systemowi operacyjnemu go zniszczyć.
GrandOpener
1
Jest bezpieczny w każdym nowoczesnym „normalnym” systemie operacyjnym (bez wbudowanej ochrony pamięci) i byłby poważnym błędem, gdyby nie był. Jeśli nieuprzywilejowany proces może trwale utracić pamięć, której system operacyjny nie odzyska, wówczas wielokrotne uruchomienie może spowodować brak pamięci w systemie. Długoterminowa kondycja systemu operacyjnego nie może zależeć od braku błędów w nieuprzywilejowanych programach. (oczywiście, to jest ignorowanie rzeczy jak Unix segmentów współużytkowanych pamięci, które są wspierane tylko przez swap w najlepsze.)
Peter Cordes
7
@GrandOpener W tym przypadku wolisz użyć pewnego rodzaju regionalnego programu przydzielającego dla tego drzewa, abyś mógł przydzielić go w normalny sposób i po prostu cofnąć alokację całego regionu na raz, zamiast nadejść i zwolnić to po trochu. To wciąż „właściwe”.
Thomas
3
Dodatkowo, jeśli pamięć naprawdę musi istnieć przez cały okres istnienia programu, rozsądną alternatywą może być utworzenie struktury na stosie, np main. W.
Kyle Strand
11

Zwolnienie pamięci pod koniec działania programu to tylko strata czasu procesora. To jest jak sprzątanie domu przed wypuszczeniem go z orbity.

Czasami jednak to, co było krótko działającym programem, może stać się częścią znacznie dłuższego programu. Wtedy konieczne staje się uwolnienie rzeczy. Jeśli nie zostało to przynajmniej w pewnym stopniu przemyślane, może wymagać znacznej przeróbki.

Jednym sprytnym rozwiązaniem tego problemu jest „talloc”, który pozwala ci dokonać mnóstwa przydziałów pamięci, a następnie wyrzucić je wszystkie za pomocą jednego połączenia.

Peter Green
źródło
1
Zakładając, że wiesz, że twój system operacyjny nie ma własnych wycieków.
WGroleau,
5
„strata czasu procesora” Chociaż bardzo, bardzo mało czasu. freejest zwykle znacznie szybszy niż malloc.
Paul Draper,
@PaulDraper Tak, strata czasu na zamianie wszystkiego z powrotem jest znacznie bardziej znacząca.
Deduplicator
5

Możesz użyć języka z funkcją odśmiecania pamięci (np. Scheme, Ocaml, Haskell, Common Lisp, a nawet Java, Scala, Clojure).

(W większości języków GC-ED, nie ma sposobu, aby wyraźnie i ręcznie wolnej pamięci Czasami niektóre wartości mogą być! Sfinalizowane , np GC i systemu wykonawczego by zamknąć wartość uchwytu pliku, gdy wartość ta jest nieosiągalny, ale to nie jest jasne , niewiarygodne, a zamiast tego należy jawnie zamknąć uchwyty plików, ponieważ finalizacja nigdy nie jest gwarantowana)

Możesz również, dla swojego programu zakodowanego w C (lub nawet C ++) użyć konserwatywnego modułu wyrzucania śmieci Boehm . Można by wtedy wymienić wszystkie swoje mallocze GC_malloc i nie przejmować free-ing dowolny wskaźnik. Oczywiście musisz zrozumieć zalety i zalety używania GC Boehm. Przeczytaj także podręcznik GC .

Zarządzanie pamięcią jest globalną własnością programu. W pewnym sensie (i żywotność niektórych danych) jest niekompozytowa i niemodularna, ponieważ cała właściwość programu.

W końcu, jak zauważyli inni, jawne jest jawne stosowanie freestrefy pamięci C przydzielonej do stosu. W przypadku programu zabawkowego, który nie przydziela dużej ilości pamięci, można w ogóle zdecydować się na brak freepamięci (ponieważ po zakończeniu procesu jego zasoby, w tym wirtualna przestrzeń adresowa , zostaną zwolnione przez system operacyjny).

Jeśli muszę użyć fragmentu pamięci przez cały okres użytkowania mojego programu, czy naprawdę trzeba go zwolnić tuż przed zakończeniem programu?

Nie, nie musisz tego robić. I wiele programów w świecie rzeczywistym nie zadaje sobie trudu, aby zwolnić trochę pamięci, która jest potrzebna przez cały okres życia (w szczególności kompilator GCC nie zwalnia części swojej pamięci). Jednak gdy to zrobisz (np. Nie freezawracasz sobie głowy konkretnym fragmentem dynamicznie przydzielanych danych C ), lepiej skomentuj ten fakt, aby ułatwić pracę przyszłym programistom przy tym samym projekcie. Zazwyczaj zalecam, aby ilość pamięci niewyleczonej pozostała ograniczona, i zwykle relatywnie mała wartość wrt do całkowitej używanej pamięci sterty.

Zauważ, że system freeczęsto nie zwalnia pamięci do systemu operacyjnego (np. Wywołując munmap (2) w systemach POSIX), ale zwykle zaznacza strefę pamięci jako nadającą się do ponownego wykorzystania w przyszłości malloc. W szczególności wirtualna przestrzeń adresowa (np. Widziana /proc/self/mapsw systemie Linux, patrz proc (5) ....) może się później nie zmniejszać free(stąd narzędzia takie jak pslub topzgłaszają taką samą ilość używanej pamięci dla twojego procesu).

Basile Starynkevitch
źródło
3
„Czasami niektóre wartości mogą zostać sfinalizowane, np. GC i system wykonawczy zamykają wartość uchwytu pliku, gdy ta wartość jest nieosiągalna”. Możesz podkreślić, że w większości języków GCed nie ma gwarancji, że pamięć zostanie odzyskana, ani dowolny finalizator kiedykolwiek działał, nawet przy wyjściu: stackoverflow.com/questions/7880569/…
Deduplicator
Osobiście wolę tę odpowiedź, ponieważ zachęca nas do korzystania z GC (tj. Bardziej zautomatyzowanego podejścia).
Ta Thanh Dinh
3
@tathanhdinh Bardziej zautomatyzowany, choć wyłącznie do pamięci. Całkowicie ręczny, dla wszystkich innych zasobów. GC działa poprzez handel determinizmem i pamięcią dla wygodnej obsługi pamięci. I nie, finalizatory niewiele pomagają i mają własne problemy.
Deduplicator
3

Nie jest to konieczne , ponieważ w przypadku niepowodzenia nie wykonasz poprawnie swojego programu. Istnieją jednak powody, które możesz wybrać, jeśli masz szansę.

Jednym z najpotężniejszych przypadków, na które napotykam (w kółko) jest to, że ktoś pisze mały fragment kodu symulacyjnego, który działa w pliku wykonywalnym. Mówią „chcielibyśmy, aby ten kod został włączony do symulacji”. Pytam ich, jak zamierzają ponownie zainicjować między biegami w Monte Carlo, i patrzą na mnie tępo. „Co oznacza ponowne zainicjowanie? Po prostu uruchamiasz program z nowymi ustawieniami?”

Czasami czyste czyszczenie znacznie ułatwia korzystanie z oprogramowania. W przypadku wielu przykładów zakładasz, że nigdy nie musisz czegoś sprzątać, i przyjmujesz założenia dotyczące sposobu obchodzenia się z danymi i ich żywotnością wokół tych domniemań. Po przejściu do nowego środowiska, w którym te domniemania są nieprawidłowe, całe algorytmy mogą nie działać.

Na przykład, jak dziwne rzeczy mogą się wydarzyć, zobacz, jak zarządzane języki radzą sobie z finalizacją na końcu procesów lub jak C # radzi sobie z programowym zatrzymywaniem domen aplikacji. Zawiązują się w węzły, ponieważ w takich ekstremalnych przypadkach pojawiają się załamania.

Cort Ammon
źródło
1

Ignorowanie języków, w których i tak nie zwalniasz pamięci ręcznie ...

To, co teraz nazywasz „programem”, może kiedyś stać się funkcją lub metodą, która jest częścią większego programu. I wtedy ta funkcja może zostać wywołana wiele razy. A potem pamięć, którą powinieneś „uwolnić ręcznie”, będzie przeciekiem pamięci. To oczywiście wezwanie do osądu.

gnasher729
źródło
2
wydaje się to jedynie powtórzeniem punktu (i znacznie lepiej wyjaśnionego) w górnej odpowiedzi, która została opublikowana kilka godzin wcześniej: „może być znacznie łatwiej przenieść kod wykorzystujący pamięć wraz z alokacją i dezalokacją do osobnego komponentu i użyć go później w innym kontekście, w którym czas użytkowania pamięci musi być kontrolowany przez użytkownika komponentu ... "
gnat
1

Ponieważ jest bardzo prawdopodobne, że za jakiś czas będziesz chciał zmienić swój program, być może zintegrować go z czymś innym, uruchomić kilka instancji w sekwencji lub równolegle. Wtedy konieczne będzie ręczne zwolnienie tej pamięci - ale nie będziesz już zapamiętywał okoliczności, a ponowne zrozumienie twojego programu będzie kosztować znacznie więcej czasu.

Rób rzeczy, dopóki twoje zrozumienie na ich temat jest wciąż świeże.

To niewielka inwestycja, która może przynieść duże zyski w przyszłości.

Agent_L
źródło
2
wydaje się, że nie dodaje to nic istotnego w stosunku do punktów przedstawionych i wyjaśnionych w poprzednich 11 odpowiedziach. W szczególności punkt o możliwej zmianie programu został już podany trzy lub cztery razy
gnat
1
@gnat W zawiły i niejasny sposób - tak. W wyraźnym oświadczeniu - nie. Skupmy się na jakości odpowiedzi, a nie szybkości.
Agent_L
1
jeśli chodzi o jakość, najlepsza odpowiedź wydaje się być znacznie lepsza w wyjaśnianiu tego
gnat
1

Nie, nie jest to konieczne, ale to dobry pomysł.

Mówisz, że czujesz, że „wydarzyłyby się tajemnicze i okropne rzeczy, gdybym nie zwolnił pamięci po jej zakończeniu”.

Z technicznego punktu widzenia jedyną konsekwencją tego jest to, że Twój program będzie zjadał więcej pamięci, dopóki nie zostanie osiągnięty twardy limit (np. Wyczerpana zostanie wirtualna przestrzeń adresowa) lub wydajność stanie się nie do przyjęcia. Jeśli program wkrótce się zakończy, nic z tego nie ma znaczenia, ponieważ proces faktycznie przestaje istnieć. „Tajemnicze i straszne rzeczy” dotyczą wyłącznie stanu psychicznego dewelopera. Znalezienie źródła wycieku pamięci może być absolutnym koszmarem (jest to mało powiedziane) i wymaga dużo umiejętności i dyscypliny, aby napisać kod, który nie przecieka. Zalecanym podejściem do rozwijania tej umiejętności i dyscypliny jest zawsze zwalnianie pamięci, gdy nie jest już potrzebne, nawet jeśli program się kończy.

Oczywiście ma to tę dodatkową zaletę, że twój kod może być ponownie użyty i dostosowany łatwiej, jak powiedzieli inni.

Istnieje jednak co najmniej jeden przypadek, w którym lepiej nie zwalniać pamięci tuż przed zakończeniem programu.

Rozważ przypadek, w którym dokonałeś milionów małych alokacji, a większość z nich została zamieniona na dysk. Kiedy zaczniesz zwalniać wszystko, większość pamięci musi zostać zamieniona z powrotem do pamięci RAM, aby można było uzyskać dostęp do informacji księgowych, tylko po to, aby natychmiast usunąć dane. Może to spowodować, że program zajmie kilka minut, aby wyjść! Nie wspominając o tym, że w tym czasie wywierał duży nacisk na dysk i pamięć fizyczną. Jeśli na początku brakuje pamięci fizycznej (być może program jest zamykany, ponieważ inny program żuje dużo pamięci), poszczególne strony mogą wymagać zamiany kilka razy, gdy kilka obiektów musi zostać uwolnionych z ta sama strona, ale nie są one zwalniane kolejno.

Jeśli zamiast tego program po prostu przerwie, system operacyjny po prostu odrzuci całą pamięć, która została zamieniona na dysk, co jest prawie natychmiastowe, ponieważ nie wymaga żadnego dostępu do dysku.

Ważne jest, aby pamiętać, że w języku OO wywołanie destruktora obiektu spowoduje również zamianę pamięci; jeśli musisz to zrobić, równie dobrze możesz zwolnić pamięć.

Artelius
źródło
1
Czy możesz przytoczyć coś, co potwierdza pomysł, że pamięć stronicowana musi być stronicowana w celu zwolnienia? To oczywiście nieefektywne, z pewnością nowoczesne systemy operacyjne są inteligentniejsze!
1
@Jon of All Trades, nowoczesne systemy operacyjne są inteligentne, ale jak na ironię większość współczesnych języków nie jest. Kiedy zwolnisz (coś), kompilator umieści „coś” na stosie, a następnie wywoła free (). Umieszczenie go na stosie wymaga zamiany z powrotem do pamięci RAM, jeśli zostanie wymieniony.
Jeffiekins
@JofofAllTrades miałaby taki efekt, jest to jednak dość przestarzała implementacja malloc. Ale system operacyjny nie może uniemożliwić korzystania z takiej implementacji.
0

Główne powody ręcznego czyszczenia to: mniej prawdopodobne jest, że pomylisz autentyczny wyciek pamięci z blokiem pamięci, który zostanie wyczyszczony po wyjściu, czasami złapiesz błędy w alokatorze tylko wtedy, gdy przejdziesz całą strukturę danych do zwalnianie go, a będziesz miał znacznie mniejszy ból głowy, jeśli kiedykolwiek byłaby tak że część obiektu będzie trzeba się zwalniane przed zakończeniem programu.

Główne powody, dla których nie należy czyścić ręcznie, to: wydajność, wydajność, wydajność oraz możliwość wystąpienia błędu typu „dwa razy za darmo” lub „użyj po zwolnieniu” w niepotrzebnym kodzie czyszczenia, który może zmienić się w błąd po awarii lub bezpieczeństwo wykorzystać.

Jeśli zależy Ci na wydajności, zawsze chcesz profilować, aby dowiedzieć się, gdzie marnujesz cały swój czas, zamiast zgadywać. Możliwym kompromisem jest zawinięcie opcjonalnego kodu czyszczącego w bloku warunkowym, pozostawienie go podczas debugowania, aby uzyskać korzyści z pisania i debugowania kodu, a następnie, jeśli tylko empirycznie stwierdzono, że jest to zbyt wiele narzut, powiedz kompilatorowi, aby pomijał go w końcowym pliku wykonywalnym. A opóźnienie zamknięcia programu prawie z definicji prawie nigdy nie znajduje się na ścieżce krytycznej.

Davislor
źródło