Dlaczego „free” w języku C nie przyjmuje liczby zwolnionych bajtów?

84

Żeby było jasne: wiem to malloci freesą zaimplementowane w bibliotece C, która zwykle alokuje fragmenty pamięci z systemu operacyjnego i samodzielnie zarządza, aby rozdzielić mniejsze partie pamięci do aplikacji i śledzi liczbę przydzielonych bajtów . To pytanie nie brzmi: Skąd wolny wie, ile za darmo .

Raczej chciałbym wiedzieć, dlaczego freepowstał w ten sposób w pierwszej kolejności. Będąc językiem niskiego poziomu, myślę, że byłoby całkiem rozsądne poprosić programistę C o śledzenie nie tylko tego, jaka pamięć została przydzielona, ​​ale także ile (w rzeczywistości często uważam, że w końcu śledzę liczbę bajtów w każdym razie Malloced). Wydaje mi się również, że jawne podanie liczby bajtów freemoże pozwolić na pewne optymalizacje wydajności, np. Alokator, który ma oddzielne pule dla różnych rozmiarów alokacji, byłby w stanie określić, z której puli zwolnić, po prostu patrząc na argumenty wejściowe, i ogólnie byłoby mniej miejsca.

Krótko mówiąc, dlaczego zostały malloci zostały freestworzone w taki sposób, że wymagają wewnętrznego śledzenia liczby przydzielonych bajtów? Czy to tylko historyczny wypadek?

Mała zmiana: kilka osób podało punkty, takie jak „a co, jeśli uwolnisz inną kwotę niż ta, którą przydzieliłeś”. Moje wyobrażone API mogłoby po prostu wymagać zwolnienia dokładnie określonej liczby przydzielonych bajtów; uwolnienie więcej lub mniej może być po prostu zdefiniowane jako UB lub implementacja. Nie chcę jednak zniechęcać do dyskusji na temat innych możliwości.

jaymmer - Przywróć Monikę
źródło
18
Ponieważ śledzenie samych przydziałów jest już uciążliwe, a nawet bardziej skomplikowałoby kod, gdybyś dodatkowo musiał śledzić rozmiar.
Jens Gustedt
11
Przychodzi mi do głowy kilka powodów: Po co zmuszać użytkownika do tego, jeśli nie musi? A jeśli użytkownik coś zepsuje? To i tak trochę zbędne pytanie. Gdyby dokonali innego wyboru, nadal pytałbyś dlaczego.
BoBTFish
15
@BoBTFish: To jest C mówimy, nie Python lub nawet C ++. Użytkownik musi już zrobić $ h! 1 tonę, którego nie musi. To nie jest powód.
user541686
4
K&R też nie ma nic do powiedzenia na ten temat. Możemy spekulować, co nam się podoba, ale myślę, że pierwotny powód może zostać utracony w historii .
BoBTFish
35
Nie możesz wymagać od programisty, aby poprawnie przekazał rozmiar bloku, ponieważ wywołujący mallocnie zna rozmiaru zwracanego bloku . mallocczęsto zwraca blok, który jest większy niż żądany. W najlepszym przypadku programista mógłby przekazać rozmiar żądany w malloc()wywołaniu, co wcale nie pomogłoby wdrażającemu free().
Ben Voigt

Odpowiedzi:

97

Jeden argument free(void *)(wprowadzony w Unix V7) ma jeszcze jedną dużą przewagę nad wcześniejszymi dwoma argumentami, o mfree(void *, size_t)których tu nie wspomniałem: jeden argument freeradykalnie upraszcza każde inne API, które działa z pamięcią sterty. Na przykład, w razie freepotrzeby, rozmiar bloku pamięci, strdupmusiałby w jakiś sposób zwrócić dwie wartości (wskaźnik + rozmiar) zamiast jednej (wskaźnik), a C sprawia, że ​​zwroty wielu wartości są znacznie bardziej uciążliwe niż zwroty jednowartościowe. Zamiast tego char *strdup(char *)musielibyśmy pisać char *strdup(char *, size_t *)albo inaczej struct CharPWithSize { char *val; size_t size}; CharPWithSize strdup(char *). (Obecnie ta druga opcja wygląda dość kusząco, ponieważ wiemy, że ciągi zakończone znakiem NUL są "najbardziej katastrofalnym błędem projektowym w historii komputerów", ale to z perspektywy czasu. Jeszcze w latach 70-tych zdolność C do obsługi łańcuchów jako prostego char *traktowania była faktycznie uważana za decydującą przewagę nad konkurentami, takimi jak Pascal i Algol .) Poza tym nie tylko strdupcierpi na ten problem - wpływa na każdy system lub zdefiniowany przez użytkownika funkcja, która przydziela pamięć sterty.

Wcześni projektanci Uniksa byli bardzo sprytnymi ludźmi i jest wiele powodów, dla których freejest lepszy niż mfreetaki, w zasadzie myślę, że odpowiedź na pytanie jest taka, że ​​zauważyli to i odpowiednio zaprojektowali swój system. Wątpię, czy znajdziesz jakikolwiek bezpośredni zapis tego, co działo się w ich głowach w momencie, gdy podjęli tę decyzję. Ale możemy sobie wyobrazić.

Udawaj, że piszesz aplikacje w C, aby działały na V6 Unix, z jego dwoma argumentami mfree. Jak dotąd radziłeś sobie dobrze, ale śledzenie tych rozmiarów wskaźników staje się coraz bardziej kłopotliwe, ponieważ twoje programy stają się coraz bardziej ambitne i wymagają coraz częstszego używania zmiennych alokowanych na stercie. Ale masz genialny pomysł: zamiast ciągłego ich kopiowania size_t, możesz po prostu napisać kilka funkcji narzędziowych, które przechowują rozmiar bezpośrednio w przydzielonej pamięci:

void *my_alloc(size_t size) {
    void *block = malloc(sizeof(size) + size);
    *(size_t *)block = size;
    return (void *) ((size_t *)block + 1);
}
void my_free(void *block) {
    block = (size_t *)block - 1;
    mfree(block, *(size_t *)block);
}

Im więcej kodu napiszesz przy użyciu tych nowych funkcji, tym bardziej będą one niesamowite. Nie tylko uczynić kod łatwiej napisać, ale również uczynić swój kod szybciej - dwie rzeczy, które często nie idą w parze! Wcześniej przekazywałeś je size_twszędzie, co powodowało dodatkowe obciążenie procesora związane z kopiowaniem i oznaczało, że trzeba było częściej rozlewać rejestry (szczególnie w przypadku argumentów funkcji dodatkowych) i marnować pamięć (ponieważ wywołania funkcji zagnieżdżonych często powodują w wielu kopiach size_tprzechowywanych w różnych ramkach stosu). W nowym systemie nadal musisz wydać pamięć na przechowywanie plikówsize_t, ale tylko raz i nigdzie nie jest kopiowany. Może się to wydawać niewielkimi korzyściami, ale pamiętaj, że mówimy o maszynach z wyższej półki z 256 KB pamięci RAM.

To cię uszczęśliwia! Więc dzielisz się swoją fajną sztuczką z brodatymi mężczyznami, którzy pracują nad następną wersją Uniksa, ale to ich nie uszczęśliwia, tylko zasmuca. Widzisz, byli właśnie w trakcie dodawania kilku nowych funkcji narzędziowych, takich jak strdup, i zdają sobie sprawę, że ludzie używający twojej fajnej sztuczki nie będą w stanie używać swoich nowych funkcji, ponieważ wszystkie ich nowe funkcje używają kłopotliwego wskaźnika + rozmiar API. I to też sprawia, że ​​jesteś smutny, ponieważ zdajesz sobie sprawę, że będziesz musiał sam przepisać dobrą strdup(char *)funkcję w każdym programie, który napiszesz, zamiast korzystać z wersji systemu.

Ale poczekaj! Jest rok 1977, a kompatybilność wsteczna nie zostanie wynaleziona przez kolejne 5 lat! A poza tym nikt poważny nie używa tej niejasnej „uniksowej” rzeczy z jej niekolorową nazwą. Pierwsza edycja K&R jest już w drodze do wydawcy, ale to żaden problem - na pierwszej stronie jest napisane, że „C nie udostępnia żadnych operacji bezpośrednio zajmujących się obiektami złożonymi, takimi jak ciągi znaków… nie ma sterty … ”. W tym momencie w historii string.hi mallocsą to rozszerzenia dostawców (!). Tak więc, sugeruje Bearded Man # 1, możemy je zmieniać, jak chcemy; dlaczego po prostu nie zadeklarujemy, że Twój trudny dzielnik jest oficjalnym dystrybutorem?

Kilka dni później Bearded Man # 2 widzi nowe API i mówi hej, czekaj, to jest lepsze niż wcześniej, ale nadal spędza całe słowo na alokację, przechowując rozmiar. Uważa to za kolejną rzecz do bluźnierstwa. Wszyscy inni patrzą na niego, jakby był szalony, bo co innego możesz zrobić? Tej nocy zostaje do późna i wymyśla nowy alokator, który w ogóle nie przechowuje rozmiaru, ale zamiast tego wnioskuje o nim w locie, wykonując czarną magię przesunięcia bitów na wartości wskaźnika i zamienia go, utrzymując nowe API na miejscu. Nowe API oznacza, że ​​nikt nie zauważa przełączenia, ale zauważa, że ​​następnego ranka kompilator zużywa o 10% mniej pamięci RAM.

A teraz wszyscy są szczęśliwi: otrzymujesz łatwiejszy do napisania i szybszy kod, Brodaty # 1 może napisać fajny prosty, strdupktórego ludzie będą naprawdę używać, a Brodaty # 2 - pewny, że zasłużył na trochę na utrzymanie - - wraca do zabawy z quinami . Wyślij to!

A przynajmniej tak mogło się stać.

Nathaniel J. Smith
źródło
14
Eee, na wszelki wypadek, gdyby było to niejasne, jest to fantazja, z dodatkowymi szczegółami dodanymi, aby zapewnić artystyczną wszechstronność. Wszelkie podobieństwo do osób żyjących lub zmarłych wynika wyłącznie z tego, że wszyscy zaangażowani wyglądali tak samo . Proszę nie mylić z aktualną historią.
Nathaniel J. Smith
5
Wygrałeś. Wydaje mi się, że jest to najbardziej prawdopodobne wyjaśnienie (i najlepsza część pisania). Nawet jeśli wszystko tutaj okaże się nieprawidłowe lub nieważne, jest to najlepsza odpowiedź na doskonałe przedstawienia brodatych mężczyzn.
jaymmer - Przywróć Monikę
Wow, odpowiedź na tej stronie brzmi wiarygodnie. +1 ode mnie.
user541686
Jeszcze lepiej - odpowiedź na tej stronie, która jest naprawdę przyjemna! +1 również.
David C. Rankin
Zastanawiam się, czy jakieś znaczące systemy Pascal wykorzystywały zebraną bezużyteczną pulę ciągów w sposób podobny do interpreterów BASIC na mikrokomputerze? Semantyka C nie działałaby z czymś takim, ale w Pascalu można by sobie z tym dobrze radzić, gdyby kod utrzymywał identyfikowalne ramki stosu (co i tak robiło wielu kompilatorów).
supercat
31

"Dlaczego freew C nie bierze liczby bajtów do zwolnienia?"

Ponieważ nie ma takiej potrzeby, a i tak nie miałoby to sensu .

Kiedy coś przydzielasz, chcesz powiedzieć systemowi, ile bajtów ma przydzielić (z oczywistych powodów).

Jednak po przydzieleniu obiektu rozmiar odzyskiwanego regionu pamięci jest teraz określany. To ukryte. To jeden ciągły blok pamięci. Nie możesz cofnąć przydziału części (zapomnijmy realloc(), że i tak nie to robi), możesz tylko cofnąć przydział całej rzeczy. Nie możesz też "zwolnić X bajtów" - albo zwalniasz blok pamięci, który dostałeś, malloc()albo nie.

A teraz, jeśli chcesz go zwolnić, możesz po prostu powiedzieć systemowi zarządzania pamięcią: „oto ten wskaźnik, free()blok, na który wskazuje”. - a menedżer pamięci będzie wiedział, jak to zrobić, albo dlatego, że domyślnie zna rozmiar, albo może nawet nie potrzebować rozmiaru.

Na przykład, większość typowych implementacji malloc()utrzymuje połączoną listę wskaźników do wolnych i przydzielonych bloków pamięci. Jeśli przekażesz wskaźnik do free(), po prostu wyszuka ten wskaźnik na liście „przydzielonych”, odłączy odpowiedni węzeł i dołączy go do listy „wolnej”. Nie potrzebował nawet rozmiaru regionu. Będzie potrzebować tych informacji tylko wtedy, gdy potencjalnie spróbuje ponownie wykorzystać dany blok.

Rogalik Paramagnetyczny
źródło
Jeśli pożyczę od ciebie 100 $, a potem ponownie pożyczę 100 $, a potem zrobię to jeszcze pięć razy, czy naprawdę obchodzi cię, że pożyczyłem od ciebie pieniądze siedem razy (chyba że faktycznie pobierasz odsetki!)? Czy po prostu obchodzi cię, że pożyczyłem od ciebie 700 dolarów? To samo: system dba tylko o pamięć, która jest nieprzydzielona, ​​nie obchodzi go (i nie musi i nie powinien) przejmować się tym, jak podzielona jest przydzielona pamięć.
user541686
1
@Mehrdad: Nie, i tak nie jest. C jednak tak. Jego głównym celem jest uczynienie rzeczy (trochę) bezpieczniejszymi. Naprawdę nie wiem, czego tutaj szukasz.
Wyścigi lekkości na orbicie
12
@ user3477950: Nie trzeba przekazywać liczby bajtów : Tak, ponieważ zostało zaprojektowane w ten sposób. OP zapytał, dlaczego został zaprojektowany w ten sposób?
Deduplicator,
5
„Ponieważ nie musi” - równie dobrze mógł być zaprojektowany tak, że tak jest.
user253751
5
@Mehrdad to zupełnie niejasna i błędna analogia. Jeśli przydzielisz 4 bajty na sto razy, z pewnością ma znaczenie, który z nich zostanie zwolniony. uwolnienie pierwszego to nie to samo, co uwolnienie drugiego. z pieniędzmi z drugiej strony nie ma znaczenia, czy najpierw
spłacisz
14

C może nie jest tak „abstrakcyjny” jak C ++, ale nadal ma być abstrakcją w stosunku do asemblacji. W tym celu z równania usuwa się szczegóły najniższego poziomu. Zapobiega to w większości przypadków wyrównywania i wypełniania, co spowodowałoby, że wszystkie programy w C byłyby nieprzenośne.

Krótko mówiąc, na tym polega cały sens pisania abstrakcji .

Lekkość wyścigów na orbicie
źródło
9
Nie wiem, co ma z tym wspólnego wyrównanie lub dopełnienie. Odpowiedź tak naprawdę nic nie odpowiada.
user541686
1
@Mehrdad C nie jest językiem x86, stara się (mniej więcej) być przenośnym, a tym samym uwalnia programistę od tego znacznego obciążenia. I tak możesz osiągnąć ten poziom na różne inne sposoby (np. Montaż inline), ale kluczem jest abstrakcja. Zgadzam się z tą odpowiedzią.
Marco A.
4
@Mehrdad: Gdybyś poprosił malloco N bajtów i zamiast tego zwrócił wskaźnik na początek całej strony (z powodu wyrównania, dopełnienia lub innych ograniczeń , użytkownik nie mógłby tego śledzić - zmuszając go do zrobienie tego przyniosłoby
efekt
3
@MichaelFoukarakis: mallocmoże po prostu zawsze zwrócić wyrównany wskaźnik bez zapisywania rozmiaru alokacji. freemoże następnie zaokrąglić w górę do odpowiedniego wyrównania, aby upewnić się, że wszystko zostało prawidłowo uwolnione. Nie wiem, gdzie jest problem.
user541686
6
@Mehrdad: Nie ma żadnej korzyści z całej tej dodatkowej pracy, o której właśnie wspomniałeś. Dodatkowo przekazanie sizeparametru freeotwierającego kolejne źródło błędów.
Michael Foukarakis
14

W rzeczywistości, w starożytnym alokatorze pamięci jądra Unix, mfree()wziął sizeargument. malloc()i mfree()zachował dwie tablice (jedną dla pamięci rdzenia, drugą dla wymiany), które zawierały informacje o wolnych adresach i rozmiarach bloków.

Nie było alokatora przestrzeni użytkownika aż do Unix V6 (programy po prostu używały sbrk()). W Unix V6, iolib zawierał z przydzielania alloc(size)i free()połączenia, które nie brały argument rozmiar. Każdy blok pamięci poprzedzony był swoim rozmiarem i wskaźnikiem do następnego bloku. Wskaźnik był używany tylko w przypadku wolnych bloków podczas przechodzenia po liście wolnych i był ponownie używany jako pamięć bloków w blokach będących w użyciu.

W Unix 32V i Unix V7 zostało to zastąpione przez nową malloc()i free()implementację, w której free()nie wziąłem sizeargumentu. Implementacja była listą cykliczną, każda porcja była poprzedzona słowem, które zawierało wskaźnik do następnej porcji oraz bit „zajętości” (przydzielony). Więc malloc()/free()nawet nie śledził wyraźnego rozmiaru.

ninjalj
źródło
10

Dlaczego freew C nie bierze liczby bajtów do zwolnienia?

Ponieważ nie musi. Informacje są już dostępne w zarządzaniu wewnętrznym prowadzonym przez malloc / free.

Oto dwie kwestie (które mogły przyczynić się do tej decyzji lub nie):

  • Dlaczego miałbyś oczekiwać, że funkcja otrzyma parametr, którego nie potrzebuje?

    (skomplikowałoby to praktycznie cały kod klienta zależny od pamięci dynamicznej i spowodowałoby zupełnie niepotrzebną redundancję w aplikacji). Śledzenie alokacji wskaźników jest już trudnym problemem. Śledzenie alokacji pamięci wraz z powiązanymi rozmiarami niepotrzebnie zwiększyłoby złożoność kodu klienta.

  • Co freew takich przypadkach zrobiłaby zmieniona funkcja?

    void * p = malloc(20);
    free(p, 25); // (1) wrong size provided by client code
    free(NULL, 10); // (2) generic argument mismatch
    

    Czy nie byłoby wolne (spowodowałoby wyciek pamięci?)? Zignorować drugi parametr? Zatrzymać aplikację, wywołując exit? Wdrożenie tego dodałoby dodatkowe punkty awarii w twojej aplikacji, dla funkcji, której prawdopodobnie nie potrzebujesz (a jeśli jej potrzebujesz, zobacz mój ostatni punkt poniżej - „wdrażanie rozwiązania na poziomie aplikacji”).

Raczej chcę wiedzieć, dlaczego w ogóle stworzono free.

Ponieważ jest to „właściwy” sposób, aby to zrobić. API powinno wymagać argumentów potrzebnych do wykonania swojej operacji, i nie więcej .

Wydaje mi się również, że jawne podanie liczby wolnych bajtów może pozwolić na pewne optymalizacje wydajności, np. Osoba dokonująca przydzielania, która ma oddzielne pule dla różnych rozmiarów alokacji, byłaby w stanie określić, z której puli zwolnić, po prostu patrząc na argumenty wejściowe, i ogólnie byłoby mniej miejsca.

Właściwe sposoby na wdrożenie tego to:

  • (na poziomie systemu) w ramach implementacji malloc - nic nie stoi na przeszkodzie, aby osoba wdrażająca bibliotekę napisała malloc, aby wewnętrznie stosował różne strategie w oparciu o otrzymany rozmiar.

  • (na poziomie aplikacji), pakując malloc i free w ramach własnych interfejsów API i używając ich zamiast tego (wszędzie w aplikacji, których możesz potrzebować).

utnapistim
źródło
6
@ user3477950: Nie trzeba przekazywać liczby bajtów : Tak, ponieważ zostało zaprojektowane w ten sposób. OP zapytał, dlaczego został zaprojektowany w ten sposób?
Deduplicator
4
„Ponieważ nie musi” - równie dobrze można było go tak zaprojektować, że rzeczywiście musi, nie zapisując tych informacji.
user253751
1
Co do punktu 2, zastanawiam się, czy free (NULL) jest zdefiniowanym zachowaniem. Aha, „Wszystkie zgodne ze standardami wersje biblioteki C traktują darmową (NULL) jako nie-op” - źródło stackoverflow.com/questions/1938735/ ...
Mawg mówi, że przywrócimy Monikę
10

Przychodzi mi na myśl pięć powodów:

  1. To wygodne. Usuwa całe obciążenie programisty i unika klasy niezwykle trudnych do śledzenia błędów.

  2. Otwiera możliwość zwolnienia części bloku. Ale ponieważ menedżerowie pamięci zwykle chcą mieć informacje o śledzeniu, nie jest jasne, co to by oznaczało?

  3. Lightness Races In Orbit to punkt odniesienia w kwestii wypełnienia i wyrównania. Charakter zarządzania pamięcią oznacza, że rzeczywisty przydzielony rozmiar prawdopodobnie różni się od rozmiaru, o który prosiłeś. Oznacza to, że freew przypadku konieczności zmiany rozmiaru i lokalizacji mallocnależałoby zmienić również faktyczny przydzielony rozmiar.

  4. I tak nie jest jasne, czy przekazanie rozmiaru przynosi jakiekolwiek rzeczywiste korzyści. Typowy menedżer pamięci ma 4-16 bajtów nagłówka na każdy fragment pamięci, w tym rozmiar. Ten nagłówek fragmentu może być wspólny dla przydzielonej i nieprzydzielonej pamięci, a gdy sąsiednie fragmenty zostaną zwolnione, mogą zostać zwinięte razem. Jeśli sprawisz, że wywołujący przechowuje wolną pamięć, możesz zwolnić prawdopodobnie 4 bajty na porcję, nie mając osobnego pola rozmiaru w przydzielonej pamięci, ale to pole rozmiaru prawdopodobnie i tak nie zostanie uzyskane, ponieważ dzwoniący musi gdzieś je przechowywać. Ale teraz ta informacja jest rozproszona w pamięci, a nie jest przewidywalnie zlokalizowana w porcji nagłówka, która i tak prawdopodobnie będzie mniej wydajna operacyjnie.

  5. Nawet jeśli to było bardziej efektywne to radykalnie nieprawdopodobne program spędza dużą ilość czasu uwalniając pamięć i tak więc korzyści byłyby bardzo małe.

Nawiasem mówiąc, Twój pomysł dotyczący oddzielnych alokatorów dla elementów o różnej wielkości można łatwo wdrożyć bez tych informacji (możesz użyć adresu, aby określić, gdzie nastąpiła alokacja). Robi się to rutynowo w C ++.

Dodano później

Inna odpowiedź, dość śmieszna, wywołała std :: podzielnik jako dowód, który freemoże działać w ten sposób, ale w rzeczywistości służy jako dobry przykład tego, dlaczego freenie działa w ten sposób. Istnieją dwie kluczowe różnice między tym, co malloc/ freedo, a tym, co robi std :: alokator. Po pierwsze, malloci freesą skierowane użytkownika - są one przeznaczone do ogólnych programistów do pracy z - natomiaststd::allocator ma na celu dostarczenie specjalistycznej alokacji pamięci do biblioteki standardowej. To dobry przykład, kiedy pierwszy z moich punktów nie ma lub nie ma znaczenia. Ponieważ jest to biblioteka, trudności z obsługą złożoności rozmiaru śledzenia są i tak ukryte przed użytkownikiem.

Po drugie, std :: alokator zawsze działa z pozycją o tym samym rozmiarze, co oznacza, że ​​możliwe jest użycie pierwotnie przekazanej liczby elementów do określenia, ile jest wolnego. To, dlaczego to różni się od freesiebie, jest ilustracyjne. W std::allocatorpozycjach, które mają być przydzielone, są zawsze tego samego, znanego rozmiaru i zawsze tego samego rodzaju, więc zawsze mają te same wymagania dotyczące dostosowania. Oznacza to, że alokator może być wyspecjalizowany w prostym przydzielaniu tablicy tych elementów na początku i rozdzielaniu ich w razie potrzeby. Nie można tego zrobić,free ponieważ nie ma sposobu, aby zagwarantować, że najlepszy rozmiar do zwrócenia to rozmiar, o który prosisz, zamiast tego znacznie bardziej wydajne jest czasami zwrócenie większych bloków niż prosi dzwoniący *, a zatem alboużytkownik lub menedżer musi śledzić dokładny faktycznie przyznany rozmiar. Przekazywanie tego rodzaju szczegółów implementacji użytkownikowi jest niepotrzebnym bólem głowy, który nie daje dzwoniącemu żadnych korzyści.

- * Jeśli ktoś nadal ma trudności ze zrozumieniem tego punktu, rozważ to: typowy alokator pamięci dodaje niewielką ilość informacji śledzenia na początek bloku pamięci, a następnie zwraca przesunięcie wskaźnika z tego. Przechowywane tutaj informacje zazwyczaj zawierają na przykład wskaźniki do następnego wolnego bloku. Załóżmy, że nagłówek ma zaledwie 4 bajty długości (co jest w rzeczywistości mniejszy niż większość prawdziwych bibliotek) i nie zawiera rozmiaru, a następnie wyobraźmy sobie, że mamy 20-bajtowy blok, gdy użytkownik prosi o 16-bajtowy blok, naiwny system zwróciłby 16-bajtowy blok, ale zostawiłby 4-bajtowy fragment, który nigdy, przenigdy nie mógłby zostać użyty, tracąc czas za każdym razemmalloczostanie wezwany. Jeśli zamiast tego menedżer po prostu zwróci 20-bajtowy blok, wówczas oszczędza te nieporządne fragmenty przed tworzeniem się i jest w stanie dokładniej alokować dostępną pamięć. Ale jeśli system ma to zrobić poprawnie bez śledzenia samego rozmiaru, wymagamy od użytkownika śledzenia - dla każdej, pojedynczej alokacji - ilości faktycznie przydzielonej pamięci, jeśli ma ją przekazać z powrotem za darmo. Ten sam argument dotyczy dopełnienia dla typów / alokacji, które nie pasują do pożądanych granic. Zatem co najwyżej wymaganie rozmiaru, który byłby łatwy do obsłużenia przez każdego rozsądnego menedżera pamięci.free przyjęcia rozmiaru jest albo (a) całkowicie bezużyteczne, ponieważ alokator pamięci nie może polegać na przekazanym rozmiarze, aby dopasować się do faktycznie przydzielonego rozmiaru, lub (b) bezcelowo wymaga, aby użytkownik wykonywał pracę śledzącą rzeczywisty

Jack Aidley
źródło
# 1 jest prawdą. # 2: Nie wiem, co masz na myśli. Nie jestem pewien co do punktu 3, wyrównanie nie wymaga przechowywania dodatkowych informacji. # 4 to rozumowanie koliste; Menedżerowie pamięci wymagają tylko tego narzutu na porcję, ponieważ przechowują rozmiar, więc nie można tego używać jako argumentu, dlaczego przechowują rozmiar. A numer 5 jest bardzo dyskusyjny.
user541686
Zaakceptowałem, a potem nie zaakceptowałem - to wygląda na dobrą odpowiedź, ale pytanie wydaje się być bardzo aktywne, myślę, że mogłem być trochę przedwczesny. To zdecydowanie daje mi do myślenia.
jaymmer - Przywróć Monikę
2
@jaymmer: Tak, to przedwczesne. Proponuję poczekać więcej niż dzień lub dwa przed przyjęciem i sugeruję, aby pomyśleć o tym samodzielnie. To naprawdę interesujące pytanie, a większość / wszystkie odpowiedzi, które otrzymasz na początku, na każde takie pytanie „dlaczego” w StackOverflow będą po prostu półautomatycznymi próbami uzasadnienia obecnego systemu, a nie faktycznego rozwiązania podstawowego pytania.
user541686
@Mehrdad: Źle zrozumiałeś, co mówię w punkcie 4. Nie mówię, że zajmie to dodatkowy rozmiar, mówię, że (a) przesunie się, kto musi przechowywać rozmiar, aby tak naprawdę nie zaoszczędzić miejsca i (b) wynikająca z tego zmiana prawdopodobnie to zrobi mniej wydajne nie więcej. Jeśli chodzi o numer 5, nie jestem przekonany, że to w ogóle dyskusyjne: mówimy - co najwyżej - o zapisaniu kilku instrukcji z bezpłatnego połączenia. W porównaniu z kosztami bezpłatnego połączenia, które będą znikome.
Jack Aidley
1
@Mehrdad: Aha, i na # 3, nie, nie wymaga dodatkowych informacji, wymaga dodatkowej pamięci. Typowy menedżer pamięci, który został zaprojektowany do pracy z wyrównaniem 16-bajtowym, zwróci wskaźnik do bloku 128-bajtowego, gdy zostanie wyświetlony monit o blok 115-bajtowy. Jeśli freewywołanie ma poprawnie przekazać rozmiar do zwolnienia, musi to wiedzieć.
Jack Aidley
5

Publikuję to tylko jako odpowiedź nie dlatego, że jest to ta, na którą masz nadzieję, ale dlatego, że uważam, że jest to jedyna wiarygodnie poprawna:

Prawdopodobnie pierwotnie uznano to za wygodne i później nie można go było ulepszyć.
Prawdopodobnie nie ma ku temu przekonującego powodu. (Ale szczęśliwie usunę to, jeśli okaże się, że jest niepoprawne.)

Nie będzie mieć korzyści, jeśli to było możliwe: można przeznaczyć jeden duży kawałek pamięci o rozmiarze wiedział wcześniej, to uwolnić się trochę w czasie - w przeciwieństwie do wielokrotnego przydzielania i zwalniania małych fragmentów pamięci. Obecnie takie zadania nie są możliwe.


Dla wielu (wielu 1 !) Z was, którzy uważają, że przekroczenie rozmiaru jest tak niedorzeczne:

Czy mogę odesłać Cię do decyzji projektowej C ++ dotyczącej std::allocator<T>::deallocate metody?

void deallocate(pointer p, size_type n);

Wszystkie obiekty w obszarze wskazanym przez powinny zostać zniszczone przed wezwaniem.n Tp
nbędzie pasować do wartości przekazanej w allocatecelu uzyskania tej pamięci.

Myślę, że będziesz miał raczej „interesujący” czas na analizę tej decyzji projektowej.


Jeśli chodzi o operator delete , okazuje się, że propozycja 2013 N3778 („ Deallocation Sized Sized C ++”) również ma to naprawić.


1 Wystarczy spojrzeć na komentarze pod pierwotnym pytaniem, aby zobaczyć, ile osób poczyniło pochopne stwierdzenia, takie jak „zapytany o rozmiar jest całkowicie bezużyteczny dla freewywołania”, aby uzasadnić brak sizeparametru.

user541686
źródło
5
W przypadku malloc()wdrożenia wyeliminowałoby to również konieczność pamiętania czegokolwiek o przydzielonym regionie, zmniejszając narzut alokacji do narzutu związanego z dopasowaniem. malloc()byłby w stanie samodzielnie wykonać całą księgowość w ramach uwolnionych fragmentów. Istnieją przypadki użycia, w których byłaby to duża poprawa. Należy jednak odradzać przydzielanie dużej porcji pamięci na kilka małych porcji, jednak spowodowałoby to drastyczne zwiększenie fragmentacji.
cmaster
1
@cmaster: Te przypadki użycia były dokładnie takie, o których mówiłem, trafiłeś w samo sedno, dzięki. Odnośnie do fragmentacji: nie wiem, jak to zwiększa fragmentację w stosunku do alternatywy, która polega na przydzielaniu i zwalnianiu pamięci na małe fragmenty.
user541686
std::allocatorprzydziela tylko elementy o określonej, znanej wielkości. Nie jest to podzielnik ogólnego przeznaczenia, w porównaniu jabłka do pomarańczy.
Jack Aidley
Wydaje mi się, że podjęto częściowo filozoficzną, częściowo projektową decyzję, aby uczynić standardową bibliotekę w C minimalnym zestawem prymitywów, z których można zbudować praktycznie wszystko - pierwotnie pomyślany jako język na poziomie systemowym i przenośny do wielu systemów to prymitywne podejście sprawia, że sens. W przypadku C ++ podjęto inną decyzję, aby uczynić standardową bibliotekę bardzo obszerną (i powiększać się w C ++ 11). Szybszy sprzęt programistyczny, większe pamięci, bardziej złożone architektury i potrzeba zajęcia się rozwojem aplikacji wyższego poziomu mogą przyczynić się do tej zmiany nacisku.
Clifford,
1
@Clifford: Dokładnie - dlatego powiedziałem, że nie ma ku temu przekonującego powodu. To tylko decyzja, która została podjęta i nie ma powodu, aby sądzić, że jest to zdecydowanie lepsze niż alternatywy.
user541686
2

malloc i free idą w parze, a każdemu „malloc” odpowiada jeden „wolny”. Dlatego też ma całkowity sens, że „bezpłatne” dopasowanie do poprzedniego „malloc” powinno po prostu zwolnić ilość pamięci przydzielonej przez to malloc - jest to większość przypadków użycia, które miałyby sens w 99% przypadków. Wyobraź sobie wszystkie błędy pamięci, gdyby wszystkie zastosowania malloc / free przez wszystkich programistów na całym świecie wymagałyby od programisty śledzenia kwoty przydzielonej w malloc, a następnie pamiętania o zwolnieniu tego samego. Scenariusz, o którym mówisz, powinien naprawdę wykorzystywać wiele funkcji Mallocs / Frees w jakiejś implementacji zarządzania pamięcią.

Marius George
źródło
5
Myślę, że „Wyobraź sobie wszystkich [...] Błędy” jest dyskusyjna, kiedy myślisz o innych fabrykach takich błędów gets, printfpętle ręczne (off-by-one), niezdefiniowanych zachowań Format-strings, konwersje niejawne, bit sztuczki, ET cetera.
Sebastian Mach
1

Sugerowałbym, że dzieje się tak, ponieważ jest bardzo wygodne, aby nie musieć ręcznie śledzić informacji o rozmiarze w ten sposób (w niektórych przypadkach), a także jest mniej podatne na błędy programisty.

Ponadto realloc potrzebowałby tych informacji księgowych, które, jak sądzę, zawierają więcej niż tylko rozmiar alokacji. tzn. pozwala na zdefiniowanie mechanizmu, za pomocą którego działa.

Możesz jednak napisać swój własny alokator, który działał w sposób, który sugerujesz, i często jest to robione w języku c ++ dla alokatorów puli w podobny sposób dla określonych przypadków (z potencjalnie ogromnym wzrostem wydajności), chociaż jest to generalnie realizowane w kategoriach operatora nowość w przydzielaniu bloków puli.

Pete
źródło
1

Nie rozumiem, jak działałby alokator, który nie śledzi rozmiaru swoich przydziałów. Gdyby tego nie zrobił, skąd wiedziałby, która pamięć jest dostępna, aby zaspokoić przyszłe mallocżądanie? Musi przynajmniej przechowywać jakąś strukturę danych zawierającą adresy i długości, aby wskazać, gdzie są dostępne bloki pamięci. (Oczywiście przechowywanie listy wolnych miejsc jest równoznaczne z przechowywaniem listy przydzielonych miejsc).

MM
źródło
Nie wymaga nawet wyraźnego pola rozmiaru. Może mieć po prostu wskaźnik do następnego bloku i przydzielony bit.
ninjalj
0

Cóż, jedyne, czego potrzebujesz, to wskaźnik, którego użyjesz do zwolnienia wcześniej przydzielonej pamięci. Ilość bajtów jest zarządzana przez system operacyjny, więc nie musisz się tym martwić. Nie byłoby konieczne pobieranie liczby przydzielonych bajtów zwracanych przez funkcję free (). Proponuję ręczny sposób zliczania liczby bajtów / pozycji przydzielonych przez uruchomiony program:

Jeśli pracujesz w Linuksie i chcesz poznać ilość bajtów / pozycji przydzielonych przez malloc, możesz stworzyć prosty program, który używa malloc raz lub n razy i wypisuje otrzymane wskaźniki. Ponadto musisz uśpić program na kilka sekund (wystarczy, abyś mógł wykonać poniższe czynności). Następnie uruchom ten program, poszukaj jego PID, napisz cd / proc / process_PID i po prostu wpisz „cat maps”. Dane wyjściowe pokażą, w jednej określonej linii, zarówno początkowy, jak i końcowy adres pamięci obszaru pamięci sterty (tego, w którym alokujesz pamięć dynamicznie). Jeśli wydrukujesz wskaźniki do tych obszarów pamięci, które są przydzielane, można odgadnąć, ile pamięci zostało przydzielone.

Mam nadzieję, że to pomoże!


źródło
0

Dlaczego miałoby to robić? malloc () i free () są celowo bardzo prostymi prymitywami zarządzania pamięcią , a zarządzanie pamięcią wyższego poziomu w języku C w dużej mierze zależy od programisty. T

Co więcej, realloc () już to robi - jeśli zmniejszysz alokację w realloc (), czy nie spowoduje to przeniesienia danych, a zwrócony wskaźnik będzie taki sam jak oryginał.

Ogólnie rzecz biorąc, cała biblioteka standardowa składa się z prostych prymitywów, z których można budować bardziej złożone funkcje, aby dopasować je do potrzeb aplikacji. Zatem odpowiedź na każde pytanie w postaci „dlaczego standardowa biblioteka nie obsługuje X” jest taka, że ​​nie może zrobić wszystkiego, o czym programiści mogą pomyśleć (po to są programiści), więc decyduje się zrobić bardzo mało - zbudować własną lub używać bibliotek innych firm. Jeśli potrzebujesz bardziej rozbudowanej biblioteki standardowej - w tym bardziej elastycznego zarządzania pamięcią, C ++ może być odpowiedzią.

Oznaczyłeś pytanie C ++, a także C, a jeśli C ++ jest tym, czego używasz, to w żadnym wypadku nie powinieneś używać malloc / free - poza new / delete, klasy kontenerów STL zarządzają pamięcią automatycznie i w sposób prawdopodobny być szczególnie odpowiednie do charakteru różnych pojemników.

Clifford
źródło
Co więcej, realloc () już to robi - jeśli zmniejszysz alokację w realloc (), czy nie spowoduje to przeniesienia danych, a zwrócony wskaźnik będzie taki sam jak oryginał. Czy to gwarantowane zachowanie? Wydaje się, że jest to powszechne w kilku osadzonych alokatorach, ale nie byłem pewien, czy to zachowanie zostało określone jako część standard-c.
rsaxvc
@rsaxvc: Dobre pytanie - dokumenty cplusplus.com "Jeśli nowy rozmiar jest większy, wartość nowo przydzielonej części jest nieokreślona." , co oznacza, że ​​jeśli jest mniejszy, jest zdeterminowany . [opengroup.org () mówi: „Jeśli nowy rozmiar obiektu pamięci wymagałby przesunięcia obiektu, miejsce na poprzednią instancję obiektu jest zwalniane”. - gdyby był mniejszy, dane nie musiałyby być przenoszone. Znowu implikacja jest taka, że ​​mniejsze nie zostaną ponownie przydzielone. Nie jestem pewien, co mówi norma ISO.
Clifford,
1
reallocjest absolutnie dozwolone do przenoszenia danych. Zgodnie ze standardem C implementacja reallocjako malloc+ memcpy+ jest całkowicie legalna free. I są dobre powody, dla których implementacja może chcieć przenieść alokację, która została zmniejszona, np. Aby uniknąć fragmentacji pamięci.
Nathaniel J. Smith