W C malloc()
przydziela region pamięci w stercie i zwraca do niego wskaźnik. To wszystko, co dostajesz. Pamięć jest niezainicjowana i nie masz gwarancji, że są to zera lub cokolwiek innego.
W Javie wywoływanie new
dokonuje alokacji stosu, podobnie jakmalloc()
, ale zyskujesz także mnóstwo dodatkowej wygody (lub obciążenia, jeśli wolisz). Na przykład nie musisz jawnie określać liczby bajtów do przydzielenia. Kompilator oblicza to dla Ciebie na podstawie typu obiektu, który próbujesz przydzielić. Dodatkowo wywoływane są konstruktory obiektów (do których można przekazywać argumenty, jeśli chcesz kontrolować sposób inicjowania). Po new
powrocie masz zagwarantowany obiekt, który został zainicjowany.
Ale tak, pod koniec rozmowy zarówno wynik malloc()
i new
wskaźniki są po prostu wskazówkami na jakąś część danych opartych na stercie.
Druga część twojego pytania dotyczy różnic między stosem a stertą. O wiele bardziej wyczerpujące odpowiedzi można znaleźć, biorąc udział w kursie poświęconym projektowaniu kompilatora (lub czytając książkę na jego temat). Pomocny byłby również kurs na temat systemów operacyjnych. Istnieje również wiele pytań i odpowiedzi na temat stosów i stosów.
Powiedziawszy to, dam ogólny przegląd, mam nadzieję, że nie jest zbyt gadatliwy i ma na celu wyjaśnienie różnic na dość wysokim poziomie.
Zasadniczo głównym powodem posiadania dwóch systemów zarządzania pamięcią, tj. Sterty i stosu, jest wydajność . Drugim powodem jest to, że każdy z nich jest lepszy w przypadku niektórych rodzajów problemów niż drugi.
Stosy są nieco łatwiejsze do zrozumienia jako koncepcja, więc zaczynam od stosów. Rozważmy tę funkcję w C ...
int add(int lhs, int rhs) {
int result = lhs + rhs;
return result;
}
Powyższe wydaje się dość proste. Definiujemy funkcję o nazwie add()
i przekazujemy w lewym i prawym uzupełnieniu. Funkcja dodaje je i zwraca wynik. Zignoruj wszystkie przypadki skrajnych przypadków, takie jak przepełnienia, które mogą wystąpić, w tym momencie nie jest to istotne dla dyskusji.
The add()
funkcji wydaje się dość prosty, ale co możemy powiedzieć o jej cyklu życia? Zwłaszcza potrzeba wykorzystania pamięci?
Co najważniejsze, kompilator wie a priori (tj. W czasie kompilacji), jak duże są typy danych i ile będzie używanych. lhs
I rhs
argumenty są sizeof(int)
4 bajty każda. Zmienna result
jest również sizeof(int)
. Kompilator może stwierdzić, żeadd()
funkcja go używa4 bytes * 3 ints
lub w sumie 12 bajtów pamięci.
Po add()
wywołaniu funkcji rejestr sprzętowy zwany wskaźnikiem stosu będzie miał w nim adres wskazujący na górę stosu. Aby przydzielić pamięć, którą add()
musi uruchomić funkcja, wszystko, co musi zrobić kod wprowadzania funkcji, to wydać jedną instrukcję języka asemblera, aby zmniejszyć wartość rejestru wskaźnika stosu o 12. W ten sposób tworzy miejsce na stosie dla trzech ints
, po jednym dla każdego lhs
, rhs
iresult
. Uzyskanie potrzebnej przestrzeni pamięci przez wykonanie pojedynczej instrukcji jest ogromną wygraną pod względem szybkości, ponieważ pojedyncze instrukcje mają tendencję do wykonywania w jednym takcie zegara (1 miliardowa sekunda procesora 1 GHz).
Ponadto, z widoku kompilatora, może utworzyć mapę do zmiennych, które wyglądają okropnie podobnie jak indeksowanie tablicy:
lhs: ((int *)stack_pointer_register)[0]
rhs: ((int *)stack_pointer_register)[1]
result: ((int *)stack_pointer_register)[2]
Znowu wszystko to jest bardzo szybkie.
Po add()
wyjściu z funkcji należy wyczyścić. Odbywa się to poprzez odjęcie 12 bajtów od rejestru wskaźnika stosu. Jest podobny do wywołania, free()
ale korzysta tylko z jednej instrukcji procesora i wymaga tylko jednego tiku. Jest bardzo, bardzo szybki.
Teraz rozważ alokację na stercie. To wchodzi w grę, gdy nie wiemy a priori, ile pamięci będziemy potrzebować (tj. Dowiemy się o tym tylko w czasie wykonywania).
Rozważ tę funkcję:
int addRandom(int count) {
int numberOfBytesToAllocate = sizeof(int) * count;
int *array = malloc(numberOfBytesToAllocate);
int result = 0;
if array != NULL {
for (i = 0; i < count; ++i) {
array[i] = (int) random();
result += array[i];
}
free(array);
}
return result;
}
Zauważ, że addRandom()
funkcja nie wie w czasie kompilacji, jaka będzie wartość count
argumentu. Z tego powodu nie ma sensu próbować zdefiniować array
tak, jak byśmy to zrobili, gdybyśmy umieszczali go na stosie:
int array[count];
Jeśli count
jest ogromny, może spowodować, że nasz stos będzie zbyt duży i zastąpi inne segmenty programu. Gdy ten stos się przepełni nastąpi , program ulega awarii (lub gorzej).
Tak więc w przypadkach, gdy nie wiemy, ile pamięci potrzebujemy do czasu wykonania, używamy malloc()
. Następnie możemy po prostu poprosić o liczbę bajtów, której potrzebujemy, gdy jej potrzebujemy, i malloc()
sprawdzimy, czy może sprzedać tyle bajtów. Jeśli to możliwe, świetnie, odzyskujemy go, jeśli nie, otrzymujemy wskaźnik NULL, który mówi nam, że wywołanie malloc()
nie powiodło się. Warto zauważyć, że program nie ulega awarii! Oczywiście jako programista możesz zdecydować, że Twój program nie będzie mógł działać, jeśli alokacja zasobów nie powiedzie się, ale zakończenie inicjowane przez programistę różni się od fałszywej awarii.
Więc teraz musimy wrócić, aby spojrzeć na wydajność. Alokator stosu jest super szybki - jedna instrukcja do alokacji, jedna instrukcja do dezalokacji i jest wykonywana przez kompilator, ale pamiętaj, że stos jest przeznaczony do takich zmiennych lokalnych o znanym rozmiarze, więc wydaje się być dość mały.
Z drugiej strony alokator sterty jest wolniejszy o kilka rzędów wielkości. Musi przejrzeć tabele, aby sprawdzić, czy ma wystarczającą ilość wolnej pamięci, aby móc sprzedać ilość pamięci, jakiej chce użytkownik. Musi zaktualizować te tabele po tym, jak vending pamięci, aby upewnić się, że nikt inny nie może użyć tego bloku (ta księgowość może wymagać od alokatora zarezerwowania pamięci dla siebie oprócz tego, co planuje sprzedać). Alokator musi zastosować strategie blokowania, aby upewnić się, że pamięć jest zabezpieczona w sposób bezpieczny dla wątków. A kiedy pamięć wreszcie się pojawifree()
d, co dzieje się w różnym czasie i zazwyczaj w nieprzewidywalnej kolejności, alokator musi znaleźć ciągłe bloki i połączyć je z powrotem, aby naprawić fragmentację hałdy. Jeśli brzmi to tak, jakbyś potrzebował więcej niż jednej instrukcji procesora, aby to wszystko osiągnąć, masz rację! To bardzo skomplikowane i zajmuje trochę czasu.
Ale stosy są duże. Dużo większy niż stosy. Możemy uzyskać od nich dużo pamięci i są świetne, gdy nie wiemy w czasie kompilacji, ile pamięci będziemy potrzebować. Wymieniamy więc prędkość na system pamięci zarządzanej, który grzecznie odmawia nam zamiast zawieszać się, gdy próbujemy przydzielić coś zbyt dużego.
Mam nadzieję, że to pomoże odpowiedzieć na niektóre twoje pytania. Daj mi znać, jeśli chcesz wyjaśnić którekolwiek z powyższych.