dlaczego rozmiar pamięci stosu jest tak ograniczony?


85

Kiedy alokujesz pamięć na stercie, jedynym ograniczeniem jest wolna pamięć RAM (lub pamięć wirtualna). To sprawia, że ​​Gb pamięci.

Dlaczego więc rozmiar stosu jest tak ograniczony (około 1 Mb)? Jaki powód techniczny uniemożliwia tworzenie naprawdę dużych obiektów na stosie?

Aktualizacja : Mój zamiar może nie być jasny, nie chcę umieszczać dużych obiektów na stosie i nie potrzebuję większego stosu. To pytanie to po prostu czysta ciekawość.


Dlaczego tworzenie dużych obiektów na stercie byłoby praktyczne? (Łańcuchy call zazwyczaj trafiają na stos.)
Makoto

4
Myślę, że prawdziwa odpowiedź jest prostsza niż większość odpowiedzi przedstawia: „ponieważ zawsze tak robiliśmy i do tej pory było dobrze, więc po co zmieniać?”
Jerry Coffin

@JerryCoffin Czy przeczytałeś jakieś odpowiedzi opublikowane do tej pory? Jest więcej wglądu w to pytanie.
user1202136

3
@ user1202136: Przeczytałem je wszystkie - ale ludzie zgadują, a przypuszczam, że wiele z przytoczonych przez nich czynników prawdopodobnie nie było nawet branych pod uwagę przy podejmowaniu pierwotnych decyzji na ten temat. Ukuwając frazę, „czasami cygaro jest tylko cygarem”.
Jerry Coffin

3
„Jak duży powinien być domyślny stos?” „Och, nie wiem, ile wątków możemy uruchomić?” „Wysadza gdzieś powyżej K” „OK, więc nazwiemy to 2K, mamy 2 Gig wirtualnego, więc co powiesz na 1 MB?” „Tak, OK, jaki jest następny numer?”
Martin James

Odpowiedzi:


46

Moja intuicja jest następująca. Stos nie jest tak łatwy do zarządzania jak sterta. Stos musi być przechowywany w ciągłych lokalizacjach pamięci. Oznacza to, że nie możesz losowo przydzielać stosu w razie potrzeby, ale musisz przynajmniej zarezerwować w tym celu adresy wirtualne. Im większy rozmiar zarezerwowanej wirtualnej przestrzeni adresowej, tym mniej wątków można utworzyć.

Na przykład aplikacja 32-bitowa ma na ogół wirtualną przestrzeń adresową 2 GB. Oznacza to, że jeśli rozmiar stosu wynosi 2 MB (domyślnie w pthreads), możesz utworzyć maksymalnie 1024 wątki. Może to być niewielkie w przypadku aplikacji, takich jak serwery internetowe. Zwiększenie rozmiaru stosu do, powiedzmy, 100 MB (tj. Rezerwujesz 100 MB, ale niekoniecznie natychmiast przydzielasz stosowi 100 MB), ograniczyłoby liczbę wątków do około 20, co może być ograniczeniem nawet dla prostych aplikacji GUI.

Ciekawym pytaniem jest, dlaczego nadal mamy ten limit na platformach 64-bitowych. Nie znam odpowiedzi, ale zakładam, że ludzie są już przyzwyczajeni do pewnych „dobrych praktyk stosu”: uważaj, aby alokować ogromne obiekty na stercie i, jeśli to konieczne, ręcznie zwiększaj rozmiar stosu. Dlatego nikt nie uznał za przydatne dodawanie obsługi „ogromnego” stosu na platformach 64-bitowych.


Wiele maszyn 64-bitowych ma tylko adresy 48-bitowe (zapewniające duży zysk w porównaniu z 32-bitowymi, ale nadal ograniczone). Nawet przy dodatkowej przestrzeni musisz się martwić o rezerwację w odniesieniu do tabel stron - to znaczy, że posiadanie większej ilości miejsca zawsze wiąże się z nadwyżkami. Prawdopodobnie jest to równie tanie, jeśli nie tańsze, przydzielenie nowego segmentu (mmap) zamiast rezerwowania ogromnych miejsc na stosy dla każdego wątku.
edA-qa mort-ora-y

4
@ edA-qamort-ora-y: Ta odpowiedź nie mówi o alokacji , chodzi o rezerwację pamięci wirtualnej , która jest prawie darmowa, a na pewno znacznie szybsza niż mmap.
Mooing Duck

33

Jeden aspekt, o którym nikt jeszcze nie wspomniał:

Ograniczony rozmiar stosu to mechanizm wykrywania i powstrzymywania błędów.

Zasadniczo głównym zadaniem stosu w C i C ++ jest śledzenie stosu wywołań i zmiennych lokalnych, a jeśli stos wyrasta poza granice, prawie zawsze jest to błąd w projekcie i / lub zachowaniu aplikacji .

Gdyby zezwolono na arbitralny wzrost stosu, te błędy (jak nieskończona rekurencja) byłyby wychwytywane bardzo późno, dopiero po wyczerpaniu zasobów systemu operacyjnego. Można temu zapobiec, ustawiając arbitralny limit rozmiaru stosu. Rzeczywisty rozmiar nie jest tak ważny, poza tym, że jest wystarczająco mały, aby zapobiec degradacji systemu.


Możesz mieć podobny problem z przydzielonymi obiektami (ponieważ pewnym sposobem zastąpienia rekursji jest ręczna obsługa stosu). To ograniczenie wymusza użycie innych sposobów (które nie są konieczne, bezpieczniejsze / prostsze / ..) (Zwróć uwagę na liczbę uwag o implementacji listy (zabawek), std::unique_ptraby napisać destruktor (i nie polegać na inteligentnym wskaźniku)).
Jarod42,

15

To tylko domyślny rozmiar. Jeśli potrzebujesz więcej, możesz uzyskać więcej - najczęściej mówiąc linkerowi, aby przydzielił dodatkową przestrzeń na stosie.

Wadą dużych stosów jest to, że jeśli utworzysz wiele wątków, będą one potrzebować po jednym stosie. Jeśli wszystkie stosy przydzielają wiele MB, ale ich nie używają, miejsce zostanie zmarnowane.

Musisz znaleźć odpowiednią równowagę dla swojego programu.


Niektórzy ludzie, jak @BJovke, uważają, że pamięć wirtualna jest zasadniczo wolna. Prawdą jest, że nie musisz mieć fizycznej pamięci stanowiącej kopię zapasową całej pamięci wirtualnej. Musisz być w stanie przynajmniej podać adresy do pamięci wirtualnej.

Jednak na typowym 32-bitowym komputerze rozmiar pamięci wirtualnej jest taki sam, jak rozmiar pamięci fizycznej - ponieważ mamy tylko 32 bity na dowolny adres, wirtualny lub nie.

Ponieważ wszystkie wątki w procesie mają tę samą przestrzeń adresową, muszą ją podzielić między siebie. A kiedy system operacyjny odegrał swoją rolę, na aplikację zostało „tylko” 2-3 GB. Ten rozmiar jest ograniczeniem zarówno dla pamięci fizycznej, jak i wirtualnej, ponieważ nie ma już więcej adresów.


Największym problemem związanym z wątkami jest to, że nie można łatwo sygnalizować obiektów stosowych do innych wątków. Albo wątek producenta musi synchronicznie czekać na zwolnienie obiektu przez wątek konsumenta, albo trzeba wykonać drogie i generujące rywalizację głębokie kopie.
Martin James

2
@MartinJames: Nikt nie mówi, że wszystkie obiekty powinny znajdować się na stosie, omawiamy, dlaczego domyślny rozmiar stosu jest mały.
Mooing Duck

Przestrzeń nie zostanie zmarnowana, rozmiar stosu jest tylko rezerwacją ciągłej wirtualnej przestrzeni adresowej. Więc jeśli ustawisz rozmiar stosu na 100 MB, ilość pamięci RAM, która będzie faktycznie używana, zależy od zużycia stosu w wątkach.
BJovke

1
@BJovke - Ale wirtualna przestrzeń adresowa będzie nadal używana. W procesie 32-bitowym jest to ograniczone do kilku GB, więc samo zarezerwowanie 20 * 100 MB spowoduje problemy.
Bo Persson,

7

Po pierwsze, stos jest ciągły, więc jeśli przydzielisz 12 MB, musisz usunąć 12 MB, jeśli chcesz zejść poniżej tego, co utworzyłeś. Również poruszanie się wokół obiektów staje się znacznie trudniejsze. Oto przykład z prawdziwego świata, który może ułatwić zrozumienie:

Powiedzmy, że układasz pudełka w całym pokoju. Które jest łatwiejsze w zarządzaniu:

  • układanie pudełek o dowolnej wadze jeden na drugim, ale kiedy chcesz coś umieścić na dole, musisz cofnąć cały stos. Jeśli chcesz wyjąć przedmiot ze stosu i przekazać go komuś innemu, musisz zdjąć wszystkie pudełka i przenieść je na stos drugiej osoby (tylko stos)
  • Umieszczasz wszystkie swoje pudełka (z wyjątkiem naprawdę małych pudełek) w specjalnym miejscu, gdzie nie układasz rzeczy na innych przedmiotach i zapisujesz, gdzie kładziesz je na kartce papieru (wskaźnik) i kładziesz papier Słup. Jeśli chcesz przekazać pudełko komuś innemu, po prostu podaj mu kartkę papieru ze swojego stosu lub po prostu daj jej kserokopię papieru i zostaw oryginał na swoim stosie. (Stos + sterta)

Te dwa przykłady to rażące uogólnienia i są pewne punkty, które są rażąco błędne w analogii, ale są na tyle bliskie, że miejmy nadzieję, że pomogą ci dostrzec zalety w obu przypadkach.


@MooingDuck Tak, ale pracujesz w wirtualnej pamięci w swoim programie, jeśli wejdę do podprogramu, wrzucę coś na stos, a następnie wrócę z podprogramu Będę musiał albo cofnąć alokację, albo przenieść utworzony przeze mnie obiekt, zanim będę mógł się rozwinąć stos, aby wrócić do miejsca, z którego przyszedłem.
Scott Chamberlain

1
chociaż mój komentarz wynikał z błędnej interpretacji (i usunąłem go), nadal nie zgadzam się z tą odpowiedzią. Usunięcie 12 MB ze szczytu stosu to dosłownie jeden kod operacji. Zasadniczo jest darmowy. Kompilatory mogą również oszukiwać regułę "stosu" i robią to, więc nie, nie muszą kopiować / przenosić obiektu przed rozwinięciem, aby go zwrócić. Więc myślę, że twój komentarz jest również niepoprawny.
Mooing Duck

Cóż, zwykle nie ma większego znaczenia, że ​​zwolnienie 12 MB zajmuje jeden kod operacyjny na stosie powyżej 100 na stercie - prawdopodobnie jest poniżej poziomu szumów rzeczywistego przetwarzania bufora 12 MB. Jeśli kompilatory chcą oszukiwać, gdy zauważą, że zwracany jest absurdalnie duży obiekt (np. Przesuwając SP przed wywołaniem, aby uczynić przestrzeń obiektu częścią stosu wywołań), to jest w porządku, TBH, programiści, którzy zwracają takie obiekty (zamiast wskaźników / referencji) są w pewnym stopniu programistyczne.
Martin James

@MartinJames: Specyfikacja C ++ mówi również, że funkcja może zwykle umieścić dane bezpośrednio w buforze docelowym i nie używać tymczasowego, więc jeśli zachowasz ostrożność, nie ma narzutu związanego z zwracaniem bufora 12 MB według wartości.
Mooing Duck

4

Pomyśl o stosie w kolejności od blisko do daleko. Rejestry są blisko procesora (szybko), stos jest nieco dalej (ale wciąż stosunkowo blisko), a sterta daleko (wolny dostęp).

Stos oczywiście znajduje się na stercie, ale mimo to, ponieważ jest używany w sposób ciągły, prawdopodobnie nigdy nie opuszcza pamięci podręcznej procesora, co czyni go szybszym niż zwykły dostęp do sterty. Jest to powód, aby utrzymywać stosy w rozsądnych rozmiarach; aby przechowywać je w pamięci podręcznej jak najwięcej. Przydzielanie obiektów z dużym stosem (prawdopodobnie automatycznie zmieniając rozmiar stosu w przypadku przepełnienia) jest sprzeczne z tą zasadą.

Jest to więc dobry paradygmat wydajności, a nie tylko pozostałość po dawnych czasach.


4
Chociaż uważam, że buforowanie odgrywa dużą rolę w powodach sztucznego zmniejszania rozmiaru stosu, muszę poprawić stwierdzenie „stos żyje na stercie”. Zarówno stos, jak i sterta znajdują się w pamięci (wirtualnie lub fizycznie).
Sebastian Hojas

W jaki sposób „blisko lub daleko” ma związek z prędkością dostępu?
Minh Nghĩa

@ MinhNghĩa Cóż, zmienne w pamięci RAM są buforowane w pamięci L2, następnie są buforowane w pamięci L1, a potem nawet te są buforowane w rejestrach. Dostęp do pamięci RAM jest wolny, do L2 jest szybszy, L1 jest jeszcze szybszy, a rejestr jest najszybszy. Myślę, że OP miał na myśli to, że zmienne przechowywane w stosie powinny być szybko dostępne, więc procesor będzie starał się jak najlepiej trzymać zmienne stosu blisko niego, dlatego chcesz, aby był mały, stąd procesor może szybciej uzyskać dostęp do zmiennych.
157 239n

1

Wiele rzeczy, do których Twoim zdaniem potrzebujesz dużego stacka, można zrobić w inny sposób.

„Algorytmy” Sedgewicka mają kilka dobrych przykładów „usuwania” rekurencji z algorytmów rekurencyjnych, takich jak QuickSort, poprzez zastąpienie rekurencji iteracją. W rzeczywistości algorytm jest nadal rekurencyjny i nadal istnieje jako stos, ale stos sortowania jest przydzielany na stercie, zamiast używać stosu środowiska wykonawczego.

(Preferuję drugie wydanie, z algorytmami podanymi w Pascalu. Można go było użyć za osiem dolarów).

Inaczej mówiąc, jeśli myślisz, że potrzebujesz dużego stosu, twój kod jest nieefektywny. Jest lepszy sposób, który zużywa mniej stosu.


-8

Myślę, że nie ma żadnego technicznego powodu, ale byłaby to dziwna aplikacja, która właśnie utworzyła tylko jeden ogromny super-obiekt na stosie. Obiektom stosu brakuje elastyczności, która staje się bardziej problematyczna wraz ze wzrostem rozmiaru - nie można ich powrócić bez ich zniszczenia ani ustawić w kolejce do innych wątków.


1
Nikt nie mówi, że wszystkie obiekty powinny znajdować się na stosie, omawiamy, dlaczego domyślny rozmiar stosu jest mały.
Mooing Duck

To nie jest małe! Ile wywołań funkcji musiałbyś przejść, aby wykorzystać 1 MB stosu? Wartości domyślne i tak można łatwo zmienić w konsolidatorze, więc pozostaje nam pytanie „po co używać stosu zamiast sterty?”
Martin James

3
jedno wywołanie funkcji. int main() { char buffer[1048576]; } To bardzo powszechny problem dla początkujących. Jasne, że istnieje łatwe obejście, ale dlaczego powinniśmy obejść rozmiar stosu?
Mooing Duck

Cóż, po pierwsze, nie chciałbym mieć 12 MB (a nawet 1 MB) wymaganego stosu nałożonego na stos każdego wątku, który wywołuje dotkniętą funkcję. To powiedziawszy, muszę się zgodzić, że 1 MB jest trochę skąpy. Byłbym zadowolony z domyślnego 100 MB, w końcu nic nie powstrzymuje mnie przed obniżeniem go do 128K w taki sam sposób, jak nic nie powstrzymuje innych programistów przed podkręceniem go.
Martin James

1
Dlaczego nie chcesz dodać 12 MB stosu do swojego wątku? Jedynym powodem jest to, że stosy są małe. To jest argument rekurencyjny.
Mooing Duck
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.