Czy można * nie * używać funkcji free () w przydzielonej pamięci?

83

Studiuję inżynierię komputerową i mam kilka kursów elektroniki. Słyszałem, od dwóch moich profesorów (z tych kursów), że jest możliwe, aby uniknąć korzystania z free()funkcji (po malloc(), calloc()itp), ponieważ przestrzenie pamięci przydzielonej prawdopodobnie nie zostaną ponownie wykorzystane przydzielić inną pamięć. Oznacza to, że na przykład, jeśli przydzielisz 4 bajty, a następnie zwolnisz je, będziesz mieć 4 bajty miejsca, które prawdopodobnie nie zostaną ponownie przydzielone: ​​będziesz mieć dziurę .

Myślę, że to szalone: ​​nie można mieć programu niebędącego zabawką, w którym przydziela się pamięć na stercie bez jej zwalniania. Ale nie mam wiedzy, aby dokładnie wyjaśnić, dlaczego jest tak ważne, że dla każdego malloc()musi istnieć plik free().

A więc: czy są kiedykolwiek okoliczności, w których byłoby właściwe użycie malloc()bez używania free()? A jeśli nie, jak mam to wyjaśnić moim profesorom?

Nacięcie
źródło
11
Nie są „błędne” - mają ważny (choć ograniczony) punkt na temat fragmentacji bardzo małych izolowanych wolnych regionów i prawdopodobnie przedstawili to nieco dokładniej, niż podałeś.
Chris Stratton,
2
Zwalnianie pamięci nie jest konieczne tylko w przypadku korzystania z pamięci zarządzanej lub ponownego wykorzystania pierwotnie przydzielonej pamięci. Podejrzewam, że powodem, dla którego powiedzieli to dwaj instruktorzy, jest to, że mówisz o konkretnym programie, który może ponownie wykorzystać pamięć. W takim przypadku nadal używałbyś free (), ale tylko na końcu aplikacji. Jesteś pewien, że nie o to chodziło?
krowe
17
@Marian: Profesor twierdził, że w C i C ++ konieczne jest zwolnienie przydzielonej pamięci w funkcji zdefiniowanej w tym samym pliku .c / .cxx, jak została ona przydzielona ... Ci ludzie czasami wydają się poważnie cierpieć na niedotlenienie z powodu mieszkania zbyt wysoko w wieży z kości słoniowej.
PlasmaHH
4
Istnieje wiele programów innych niż zabawki, które nie zwalniają pamięci, a umożliwienie systemowi operacyjnemu wyczyszczenia wszystkiego po zakończeniu procesu jest znacznie szybsze niż (kłopotliwe) prowadzenie dużej ilości księgowości, aby można było to zrobić samodzielnie.
Donal Fellows
9
Nigdy nie łącz bezsprzecznie rzeczy, które usłyszałeś w swoim mózgu. Miałem wielu nauczycieli, lektorów i korektorów, którzy byli w błędzie lub nieaktualni. I zawsze dokładnie analizuj to, co mówią. Nasi ludzie są często dość precyzyjni i mogą mówić rzeczy, które są poprawne, ale łatwo je zrozumieć źle lub z niewłaściwym priorytetem dla kogoś, kto czuje się w domu tylko w potocznym języku. Np. Pamiętam, że w szkole nauczyciel powiedział: „Odrobiłeś lekcje”, a ja powiedziałem „Nie”. O ile miałem rację, nauczyciel uznał to za obraźliwe, ponieważ zaoszczędziłem mi czasu na szukanie kiepskich wymówek, których się nie spodziewał.
Sebastian Mach

Odpowiedzi:

100

Łatwe: po prostu przeczytaj źródło prawie każdej półpoważnej malloc()/free()implementacji. Rozumiem przez to faktycznego menedżera pamięci, który zajmuje się pracą połączeń. Może to być biblioteka środowiska wykonawczego, maszyna wirtualna lub system operacyjny. Oczywiście kod nie jest jednakowo dostępny we wszystkich przypadkach.

Bardzo często jest upewnianie się, że pamięć nie jest pofragmentowana, poprzez łączenie sąsiednich otworów w większe otwory. Poważniejsze podzielniki wykorzystują poważniejsze techniki, aby to zapewnić.

Więc załóżmy, że wykonujesz trzy alokacje i de-alokacje i otrzymujesz bloki ułożone w pamięci w następującej kolejności:

+-+-+-+
|A|B|C|
+-+-+-+

Rozmiary poszczególnych przydziałów nie mają znaczenia. następnie uwalniasz pierwszy i ostatni, A i C:

+-+-+-+
| |B| |
+-+-+-+

kiedy w końcu uwolnisz B, otrzymasz (początkowo, przynajmniej w teorii):

+-+-+-+
| | | |
+-+-+-+

które można zdefragmentować na sprawiedliwe

+-+-+-+
|     |
+-+-+-+

tj. jeden większy wolny blok, brak fragmentów.

Referencje na żądanie:

rozwijać
źródło
1
Czy możesz podać mi jakieś referencje?
Nick,
4
Myślę, że warto wspomnieć, że wirtualna przestrzeń adresowa nie jest bezpośrednią reprezentacją pamięci fizycznej. Z fragmentacją pamięci fizycznej może sobie poradzić system operacyjny, podczas gdy pamięć wirtualna, która nie została zwolniona przez proces, również nie zostanie zwolniona fizycznie.
okrążenie
@PetrBudnik rzadko by się zdarzało, że pamięć wirtualna odwzorowuje 1-1 na pamięć fizyczną, system operacyjny pomyśli o mapowaniu stron i będzie w stanie zamienić go i wymieniać przy minimalnym zamieszaniu
ratchet freak
3
Bardzo dziwaczny komentarz: chociaż koncepcja jest dobrze wyjaśniona, faktyczny przykład został wybrany nieco ... pechowy. Dla każdego, kto patrzy na kod źródłowy, powiedzmy, dlmalloc i gubi się: Bloki poniżej określonego rozmiaru są zawsze potęgami 2 i są odpowiednio łączone / dzielone. Tak więc skończylibyśmy (prawdopodobnie) z jednym blokiem 8-bajtowym i 1 blokiem 4-bajtowym, ale bez bloku 12-bajtowego. Jest to dość standardowe podejście w przypadku alokatorów, przynajmniej na komputerach stacjonarnych, chociaż być może aplikacje osadzone starają się być bardziej ostrożne ze swoim narzutem.
Voo
@Voo Usunąłem wzmiankę o rozmiarze bloków w przykładzie, i tak nie miało to znaczenia. Lepszy?
relaks
42

Inne odpowiedzi już doskonale wyjaśniają, że rzeczywiste implementacje malloc()i free()rzeczywiście łączą (defragmnent) dziury w większe wolne fragmenty. Ale nawet gdyby tak nie było, nadal byłoby złym pomysłem rezygnować free().

Chodzi o to, że twój program właśnie przydzielił (i chce zwolnić) te 4 bajty pamięci. Jeśli ma działać przez dłuższy czas, jest całkiem prawdopodobne, że będzie musiał ponownie przydzielić tylko 4 bajty pamięci. Więc nawet jeśli te 4 bajty nigdy nie połączą się w większą ciągłą przestrzeń, nadal mogą być ponownie wykorzystane przez sam program.

Angew nie jest już dumny z SO
źródło
6
+1 Dokładnie. Chodzi o to, że jeśli freejest wywoływane wystarczająco dużo razy, aby mieć wpływ na wydajność, prawdopodobnie jest również wywoływane wystarczająco dużo razy, aby pozostawienie go spowoduje bardzo duże uszkodzenie dostępnej pamięci. Trudno wyobrazić sobie sytuację w systemie wbudowanym, w której wydajność stale cierpi z powodu, freeale mallocjest nazywana skończoną liczbą razy; dość rzadkim przypadkiem użycia jest posiadanie urządzenia wbudowanego, które wykonuje jednorazowe przetwarzanie danych, a następnie resetuje się.
Jason C
10

To totalny nonsens, na przykład istnieje wiele różnych implementacji malloc, niektóre próbują uczynić stertę bardziej wydajną, jak Doug Lea lub ta .

Paul Evans
źródło
9

Czy Twoi profesorowie przypadkiem pracują z POSIX? Jeśli są przyzwyczajeni do pisania wielu małych, minimalistycznych aplikacji powłoki, to jest to scenariusz, w którym mogę sobie wyobrazić, że takie podejście nie byłoby takie złe - uwolnienie całej sterty naraz w czasie wolnym przez system operacyjny jest szybsze niż zwolnienie tysiąc zmiennych. Jeśli spodziewasz się, że Twoja aplikacja będzie działać przez sekundę lub dwie, możesz łatwo uciec bez żadnego cofnięcia alokacji.

Oczywiście nadal jest to zła praktyka (poprawa wydajności powinna zawsze opierać się na profilowaniu, a nie na niejasnym przeczuciu) i nie jest to coś, co powinieneś mówić uczniom bez wyjaśniania innych ograniczeń, ale mogę sobie wyobrazić wiele małych rur -aplikacje zapisywane w ten sposób (jeśli nie używają bezpośrednio alokacji statycznej). Jeśli pracujesz nad czymś, co przynosi korzyści wynikające z braku zwalniania zmiennych, albo pracujesz w ekstremalnie niskich warunkach (w takim przypadku, jak możesz sobie pozwolić na alokację dynamiczną i C ++?: D), albo jesteś robienie czegoś bardzo, bardzo złego (na przykład przydzielanie tablicy liczb całkowitych przez przydzielanie tysiąca liczb całkowitych jedna po drugiej zamiast pojedynczego bloku pamięci).

Luaan
źródło
Pozwolenie systemowi operacyjnemu na uwolnienie wszystkiego na końcu to nie tylko kwestia wydajności - wymaga również znacznie mniej logiki, aby zacząć działać.
hugomg
@missingno Innymi słowy, pozwala ci uciec od tego, jak trudne może być zarządzanie pamięcią :) Jednak uznałbym to za argument przeciwko niezarządzanym językom - jeśli twoim powodem jest bardziej skomplikowana logika, a nie wydajność, możesz być lepszy od używania języka / środowiska, które dba o to za Ciebie.
Luaan
5

Wspomniałeś, że byli profesorami elektroniki. Mogą być używane do pisania oprogramowania sprzętowego / oprogramowania w czasie rzeczywistym, mogą być w stanie dokładnie określić czas, w którym często potrzebne jest wykonanie niektórych czynności. W takich przypadkach wiedza, że ​​masz wystarczającą ilość pamięci na wszystkie alokacje i brak zwalniania i ponownego przydzielania pamięci, może dać łatwiejszy do obliczenia limit czasu wykonania.

W niektórych schematach sprzętowa ochrona pamięci może być również używana, aby upewnić się, że procedura zakończy się w przydzielonej jej pamięci lub wygeneruje pułapkę w bardzo wyjątkowych przypadkach.

Paddy3118
źródło
10
Trafne spostrzeżenie. Spodziewałbym się jednak, że w tym przypadku nie będą używać malloci takich w ogóle, zamiast polegać na alokacji statycznej (lub być może przydzielić dużą porcję, a następnie ręcznie obsłużyć pamięć).
Luaan,
2

Patrząc z innego punktu widzenia niż poprzedni komentatorzy i odpowiedzi, jedną z możliwości jest to, że twoi profesorowie mieli doświadczenie z systemami, w których pamięć była przydzielana statycznie (tj. Kiedy program był kompilowany).

Alokacja statyczna ma miejsce, gdy wykonujesz takie czynności, jak:

define MAX_SIZE 32
int array[MAX_SIZE];

W wielu systemach czasu rzeczywistego i systemach wbudowanych (tych, które są najczęściej spotykane przez EE lub CE), zazwyczaj lepiej jest całkowicie unikać dynamicznej alokacji pamięci. Więc zastosowań malloc, newa ich odpowiednikami skreślenie są rzadkie. Co więcej, pamięć w komputerach eksplodowała w ostatnich latach.

Jeśli masz do dyspozycji 512 MB i przydzielasz statycznie 1 MB, masz około 511 MB do przetoczenia, zanim oprogramowanie eksploduje (cóż, niezupełnie ... ale idź ze mną tutaj). Zakładając, że masz 511 MB do nadużywania, jeśli zmniejszysz 4 bajty na sekundę bez ich zwalniania, będziesz mógł działać przez prawie 73 godziny, zanim zabraknie pamięci. Biorąc pod uwagę, że wiele maszyn jest wyłączanych raz dziennie, oznacza to, że w Twoim programie nigdy nie zabraknie pamięci!

W powyższym przykładzie wyciek wynosi 4 bajty na sekundę lub 240 bajtów / min. Teraz wyobraź sobie, że zmniejszasz ten stosunek bajtów / min. Im niższy współczynnik, tym dłużej program może działać bez problemów. Jeśli twoje mallocsą rzadkie, jest to prawdziwa możliwość.

Heck, jeśli wiesz, że robisz malloccoś tylko raz, a to mallocjuż nigdy nie zostanie trafione, to jest to bardzo podobne do alokacji statycznej, chociaż nie musisz znać rozmiaru tego, co przydzielasz - z przodu. Np .: Powiedzmy, że mamy znowu 512 MB. Potrzebujemy malloc32 tablic liczb całkowitych. Są to typowe liczby całkowite - 4 bajty każda. Wiemy, że rozmiary tych tablic nigdy nie przekroczą 1024 liczb całkowitych. W naszym programie nie występują żadne inne alokacje pamięci. Czy mamy wystarczająco dużo pamięci? 32 * 1024 * 4 = 131,072. 128 KB - więc tak. Mamy dużo miejsca. Jeśli wiemy, że nigdy nie przydzielimy więcej pamięci, możemy bezpieczniemallocte tablice bez ich zwalniania. Może to jednak również oznaczać, że musisz ponownie uruchomić maszynę / urządzenie, jeśli program się zawiesi. Jeśli uruchomisz / zatrzymasz program 4096 razy, przydzielisz wszystkie 512 MB. Jeśli masz procesy zombie, możliwe, że pamięć nigdy nie zostanie zwolniona, nawet po awarii.

Oszczędź sobie bólu i nieszczęścia i spożywaj tę mantrę jako Jedyną Prawdę: zawszemalloc powinna być związana z . powinien zawsze mieć . freenewdelete

Shaz
źródło
2
W większości systemów wbudowanych i działających w czasie rzeczywistym bomba zegarowa powodująca awarię już po 73 godzinach byłaby poważnym problemem.
Ben Voigt,
typowe liczby całkowite ??? Liczba całkowita to co najmniej 16-bitowa liczba, a na małych mikroprocesorach jest zwykle 16-bitowa. Generalnie jest więcej urządzeń z sizeof(int)równymi 2 zamiast 4.
ST3
2

Myślę, że twierdzenie zawarte w pytaniu jest nonsensowne, jeśli wziąć je dosłownie z punktu widzenia programisty, ale ma prawdę (przynajmniej część) z punktu widzenia systemu operacyjnego.

malloc () w końcu wywoła albo mmap (), albo sbrk (), co spowoduje pobranie strony z systemu operacyjnego.

W każdym nietrywialnym programie szanse, że ta strona kiedykolwiek zostanie zwrócona systemowi operacyjnemu w czasie trwania procesu, są bardzo małe, nawet jeśli zwolnisz () większość przydzielonej pamięci. Tak więc pamięć free () 'd będzie dostępna tylko dla tego samego procesu przez większość czasu, ale nie dla innych.

mfro
źródło
2

Twoi profesorowie nie mylą się, ale też są (są przynajmniej mylący lub nadmiernie upraszczający). Fragmentacja pamięci powoduje problemy z wydajnością i efektywnym wykorzystaniem pamięci, więc czasami trzeba to wziąć pod uwagę i podjąć działania, aby tego uniknąć. Jedną z klasycznych sztuczek jest to, że jeśli przydzielasz wiele rzeczy o tym samym rozmiarze, przechwytujesz pulę pamięci podczas uruchamiania, która jest wielokrotnością tego rozmiaru i zarządzasz jej całkowicie wewnętrznie, zapewniając w ten sposób, że nie ma fragmentacji Poziom systemu operacyjnego (a dziury w module mapowania pamięci wewnętrznej będą miały dokładnie odpowiedni rozmiar dla następnego obiektu tego typu, który się pojawi).

Istnieją całe biblioteki innych firm, które nic nie robią, ale zajmują się tego rodzaju rzeczami za Ciebie, a czasami jest to różnica między akceptowalną wydajnością a czymś, co działa zbyt wolno. malloc()i free()zajmie zauważalną ilość czasu na wykonanie, co zaczniesz zauważać, jeśli często do nich dzwonisz.

Więc unikając tylko naiwnie użyciu malloc()i free()można uniknąć fragmentacji i wydajności zarówno problemy - ale gdy pojawi się aż do niego, należy zawsze upewnić się, że free()wszystko, czego malloc()chyba że masz bardzo dobry powód, aby zrobić inaczej. Nawet w przypadku korzystania z wewnętrznej puli pamięci dobra aplikacja będzie korzystać free()z pamięci puli przed jej zamknięciem. Tak, system operacyjny wyczyści to, ale jeśli cykl życia aplikacji zostanie później zmieniony, łatwo będzie zapomnieć, że pula wciąż się kręci ...

Długotrwałe aplikacje muszą oczywiście bardzo skrupulatnie czyścić lub przetwarzać wszystko, co przydzielili, w przeciwnym razie zabraknie im pamięci.

Matthew Walton
źródło
1

Twoi profesorowie podnoszą ważną kwestię. Niestety użycie języka angielskiego jest takie, że nie jestem absolutnie pewien, co zostało powiedziane. Pozwól, że odpowiem na to pytanie w kategoriach programów niebędących zabawkami, które mają pewne cechy wykorzystania pamięci iz którymi osobiście pracowałem.

Niektóre programy dobrze się zachowują. Alokują pamięć falami: wiele małych lub średnich alokacji, po których następuje wiele zwolnień, w powtarzających się cyklach. W tych programach typowe alokatory pamięci radzą sobie raczej dobrze. Łączą uwolnione bloki i pod koniec fali większość wolnej pamięci jest w dużych, ciągłych fragmentach. Te programy są dość rzadkie.

Większość programów źle się zachowuje. Alokują i zwalniają pamięć mniej lub bardziej losowo, w różnych rozmiarach, od bardzo małych do bardzo dużych, i zachowują wysokie wykorzystanie przydzielonych bloków. W programach tych zdolność łączenia bloków jest ograniczona iz czasem kończą się one dużą fragmentacją pamięci i względnie nieciągłością. Jeśli całkowite użycie pamięci przekracza około 1,5 GB w 32-bitowej przestrzeni pamięci, a alokacje wynoszą (powiedzmy) 10 MB lub więcej, w końcu jeden z dużych przydziałów zakończy się niepowodzeniem. Te programy są powszechne.

Inne programy zwalniają niewiele pamięci lub nie zajmują jej wcale, dopóki się nie zatrzymają. Stopniowo alokują pamięć podczas pracy, zwalniając tylko małe ilości, a następnie zatrzymują się, po czym cała pamięć jest zwalniana. Kompilator jest taki. Tak jest z maszyną wirtualną. Na przykład środowisko wykonawcze .NET CLR, samo napisane w C ++, prawdopodobnie nigdy nie zwalnia pamięci. Dlaczego miałoby to robić?

I to jest ostateczna odpowiedź. W przypadkach, w których program zajmuje wystarczająco dużo pamięci, zarządzanie pamięcią za pomocą malloc i free nie jest wystarczającą odpowiedzią na problem. O ile nie masz szczęścia, aby mieć do czynienia z dobrze działającym programem, będziesz musiał zaprojektować jeden lub więcej niestandardowych alokatorów pamięci, które wstępnie alokują duże fragmenty pamięci, a następnie przydzielają podrzędnie zgodnie z wybraną strategią. Nie możesz w ogóle używać darmowego, z wyjątkiem sytuacji, gdy program się zatrzyma.

Nie wiedząc dokładnie, co powiedzieli twoi profesorowie, w przypadku programów o prawdziwie produkcyjnej skali prawdopodobnie stanąłbym po ich stronie.

EDYTOWAĆ

Spróbuję odpowiedzieć na niektóre uwagi krytyczne. Oczywiście SO nie jest dobrym miejscem na tego typu posty. Dla jasności: mam około 30 lat doświadczenia w pisaniu tego rodzaju oprogramowania, w tym kilku kompilatorów. Nie mam żadnych akademickich odniesień, tylko własne siniaki. Nie mogę oprzeć się wrażeniu, że krytyka pochodzi od ludzi z dużo węższym i krótszym doświadczeniem.

Powtórzę mój kluczowy komunikat: zbalansowanie malloc i free nie jest wystarczającym rozwiązaniem do alokacji pamięci na dużą skalę w rzeczywistych programach. Blok koalescencyjny jest normalne, i kupuje czas, ale to nie wystarczy. Potrzebujesz poważnych, sprytnych alokatorów pamięci, które mają tendencję do przechwytywania pamięci w fragmentach (za pomocą malloc lub cokolwiek innego) i rzadko zwalniają. To prawdopodobnie przesłanie, które mieli na myśli profesorowie OP, które źle zrozumiał.

david.pfx
źródło
1
Źle zachowujące się programy pokazują, że przyzwoite alokatory powinny używać oddzielnych pul dla fragmentów o różnych rozmiarach. W przypadku pamięci wirtualnej posiadanie wielu różnych pul oddalonych od siebie nie powinno stanowić większego problemu. Jeśli każdy fragment jest zaokrąglany do potęgi dwóch, a każda pula zawiera zaokrąglone fragmenty tylko jednego rozmiaru, nie widzę, jak fragmentacja mogłaby kiedykolwiek stać się bardzo poważna. W najgorszym przypadku program może nagle całkowicie przestać interesować się pewnym zakresem rozmiarów, pozostawiając niektóre w większości puste pule niewykorzystane; Nie wydaje mi się jednak, żeby to było zbyt częste zachowanie.
Marc van Leeuwen
5
Bardzo potrzebne jest odniesienie do twierdzenia, że ​​kompetentnie napisane programy nie zwalniają pamięci, dopóki aplikacja nie zostanie zamknięta.
12
Cała odpowiedź brzmi jak seria przypadkowych domysłów i przypuszczeń. Czy jest coś na poparcie tego wszystkiego?
Chris Hayes,
4
Nie jestem pewien, co masz na myśli mówiąc, że środowisko wykonawcze .NET CLR nie zwalnia żadnej pamięci. O ile to przetestowałem, jeśli to możliwe.
Theodoros Chatzigiannakis
1
@vonbrand: GCC ma wiele alokatorów, w tym własną markę garbage collectora. Zużywa pamięć podczas przejazdów i gromadzi ją między przejściami. Większość kompilatorów dla innych języków ma co najwyżej 2 przebiegi i wolną niewiele pamięci między przebiegami lub nie ma jej wcale. Jeśli się nie zgadzasz, z przyjemnością zbadam każdy przykład, który podasz.
david.pfx
1

Dziwię się, że nikt jeszcze nie cytował Księgi :

Ostatecznie może to nie być prawdą, ponieważ pamięci mogą stać się na tyle duże, że zabraknie wolnej pamięci przez cały okres eksploatacji komputera. Na przykład jest około 3 about 10 13 mikrosekund w ciągu roku, więc gdybyśmy mieli pomijać raz na mikrosekundę, potrzebowalibyśmy około 10 15 komórek pamięci, aby zbudować maszynę, która mogłaby działać przez 30 lat bez wyczerpywania się pamięci. Taka ilość pamięci wydaje się absurdalnie duża jak na dzisiejsze standardy, ale nie jest to fizycznie niemożliwe. Z drugiej strony procesory stają się coraz szybsze, a przyszły komputer może mieć dużą liczbę procesorów działających równolegle w jednej pamięci, więc może być możliwe użycie pamięci znacznie szybciej, niż zakładaliśmy.

http://sarabander.github.io/sicp/html/5_002e3.xhtml#FOOT298

Tak więc, rzeczywiście, wiele programów radzi sobie dobrze, nie zawracając sobie głowy zwalnianiem pamięci.

Oakad
źródło
1

Wiem o jednym przypadku, w którym jawne zwolnienie pamięci jest gorsze niż bezużyteczne . Oznacza to, że potrzebujesz wszystkich danych do końca życia procesu. Innymi słowy, gdy ich zwolnienie jest możliwe tylko tuż przed zakończeniem programu. Ponieważ każdy nowoczesny system operacyjny dba o zwalnianie pamięci, gdy program umiera, free()w takim przypadku wywoływanie nie jest konieczne. W rzeczywistości może to spowolnić zakończenie programu, ponieważ może wymagać dostępu do kilku stron w pamięci.

Miklós Homolya
źródło