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?
źródło
Odpowiedzi:
Ł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:
Rozmiary poszczególnych przydziałów nie mają znaczenia. następnie uwalniasz pierwszy i ostatni, A i C:
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:
heap4.c
kodu w programie FreeRTOS .źródło
Inne odpowiedzi już doskonale wyjaśniają, że rzeczywiste implementacje
malloc()
ifree()
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.
źródło
free
jest 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,free
alemalloc
jest 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ę.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 .źródło
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).
źródło
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.
źródło
malloc
i 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ęć).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
,new
a 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
malloc
są rzadkie, jest to prawdziwa możliwość.Heck, jeśli wiesz, że robisz
malloc
coś tylko raz, a tomalloc
już 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. Potrzebujemymalloc
32 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 bezpieczniemalloc
te 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ę: zawsze
malloc
powinna być związana z . powinien zawsze mieć .free
new
delete
źródło
sizeof(int)
równymi 2 zamiast 4.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.
źródło
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()
ifree()
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()
ifree()
można uniknąć fragmentacji i wydajności zarówno problemy - ale gdy pojawi się aż do niego, należy zawsze upewnić się, żefree()
wszystko, czegomalloc()
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.
źródło
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ł.
źródło
Dziwię się, że nikt jeszcze nie cytował Księgi :
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.
źródło
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.źródło