Dlaczego rozmiar stosu w języku C # wynosi dokładnie 1 MB?

102

Dzisiejsze komputery PC mają dużą ilość fizycznej pamięci RAM, ale nadal rozmiar stosu C # wynosi tylko 1 MB dla procesów 32-bitowych i 4 MB dla procesów 64-bitowych ( pojemność stosu w C # ).

Dlaczego rozmiar stosu w CLR jest nadal tak ograniczony?

A dlaczego to dokładnie 1 MB (4 MB) (a nie 2 MB lub 512 KB)? Dlaczego zdecydowano się wykorzystać te kwoty?

Interesują mnie względy i uzasadnienie tej decyzji .

Nikolay Kostov
źródło
6
Domyślny rozmiar stosu dla procesów 64-bitowych to 4 MB, a dla procesów 32-bitowych 1 MB. Państwo może modyfikować wielkość głównego gwinty stos zmieniając wartość w nagłówku PE. Możesz również określić rozmiar stosu, używając odpowiedniego przeciążenia Threadkonstruktora. ALE, to nasuwa pytanie, dlaczego potrzebujesz większego stacka?
Yuval Itzchakov
2
Dzięki, zredagowano. :) Pytanie nie dotyczy tego, jak używać większego rozmiaru stosu, ale dlaczego zdecydowano się na 1 MB (4 MB) .
Nikolay Kostov
8
Ponieważ każdy wątek domyślnie otrzyma ten rozmiar stosu, a większość wątków nie potrzebuje tak dużo. Właśnie uruchomiłem komputer i obecnie system obsługuje 1200 wątków. A teraz policz;)
Lucas Trzesniewski
2
@LucasTrzesniewski Nie tylko to, to musi być zaraźliwe w pamięci . Zwróć uwagę, że im większy rozmiar stosu, tym mniej wątków Twój proces może utworzyć w swojej wirtualnej przestrzeni adresowej.
Yuval Itzchakov
Nie mam pewności co do „dokładnie” 1 MB: w moim systemie Windows 8.1 aplikacja konsoli .NET Core 3.1 ma 1572864domyślny rozmiar stosu w bajtach (pobierany za pomocą interfejsu API GetCurrentThreadStackLimits Win32). Jestem w stanie w stackallocprzybliżeniu 1500000bajtów bez StackOverflowException.
George Chakhidze

Odpowiedzi:

210

wprowadź opis obrazu tutaj

Patrzysz na faceta, który dokonał tego wyboru. David Cutler i jego zespół wybrali jeden megabajt jako domyślny rozmiar stosu. Nie ma to nic wspólnego z .NET czy C #, zostało to ustalone podczas tworzenia systemu Windows NT. Jeden megabajt jest tym, co wybiera, gdy nagłówek EXE programu lub wywołanie winapi CreateThread () nie określa jawnie rozmiaru stosu. Co jest normalne, prawie każdy programista pozostawia systemowi operacyjnemu wybór rozmiaru.

Ten wybór prawdopodobnie poprzedza projekt Windows NT, historia jest o wiele zbyt niejasna. Byłoby miło, gdyby Cutler napisał o tym książkę, ale nigdy nie był pisarzem. Był niezwykle wpływowy na sposób działania komputerów. Jego pierwszym projektem systemu operacyjnego był RSX-11M, 16-bitowy system operacyjny dla komputerów DEC (Digital Equipment Corporation). Ma to duży wpływ na CP / M Gary'ego Kildalla, pierwszy przyzwoity system operacyjny dla 8-bitowych mikroprocesorów. Co mocno wpłynęło na MS-DOS.

Jego kolejnym projektem był VMS, system operacyjny dla 32-bitowych procesorów z obsługą pamięci wirtualnej. Bardzo udany. Jego następny został odwołany przez DEC mniej więcej w czasie, gdy firma zaczęła się rozpadać, nie będąc w stanie konkurować z tanim sprzętem PC. Cue Microsoft, złożyli mu ofertę, której nie mógł odmówić. Przyłączyło się też wielu jego współpracowników. Pracowali na VMS v2, lepiej znanym jako Windows NT. DEC był tym zdenerwowany, pieniądze zmieniły właściciela, aby to uregulować. Nie wiem, czy VMS wybrał już jeden megabajt, ale znam RSX-11 wystarczająco dobrze. Nie jest to nieprawdopodobne.

Dość historii. Jeden megabajt to dużo , prawdziwy wątek rzadko zużywa więcej niż kilka garści kilobajtów. Więc megabajt jest raczej marnotrawstwem. Jest to jednak rodzaj marnotrawstwa, na jaki można sobie pozwolić w systemie operacyjnym pamięci wirtualnej ze stronicowaniem na żądanie, ponieważ megabajt to tylko pamięć wirtualna . Tylko numery do procesora, po jednym na każde 4096 bajtów. W rzeczywistości nigdy nie używasz pamięci fizycznej, pamięci RAM w komputerze, dopóki nie zajmiesz się tym.

W programie .NET jest on wyjątkowo nadmierny, ponieważ pierwotnie wybrano rozmiar jednego megabajta, aby pomieścić programy natywne. Które mają tendencję do tworzenia dużych ramek stosu, przechowując również ciągi i bufory (tablice) na stosie. Niesławny z tego, że jest wektorem ataku złośliwego oprogramowania, przepełnienie buforu może manipulować programem za pomocą danych. Nie sposób, w jaki działają programy .NET, łańcuchy i tablice są przydzielane na stercie GC, a indeksowanie jest sprawdzane. Jedynym sposobem przydzielenia miejsca na stosie za pomocą C # jest użycie niebezpiecznego słowa kluczowego stackalloc .

Jedynym nietrywialnym zastosowaniem stosu w .NET jest jitter. Używa stosu twojego wątku do kompilacji MSIL na czas do kodu maszynowego. Nigdy nie widziałem ani nie sprawdzałem, ile miejsca zajmuje, raczej zależy to od natury kodu i tego, czy optymalizator jest włączony, ale kilka dziesiątek kilobajtów to zgadywanie. W przeciwnym razie ta witryna otrzymała swoją nazwę, przepełnienie stosu w programie .NET jest fatalne. Za mało miejsca (mniej niż 3 kilobajty), aby nadal niezawodnie JIT był dowolny kod, który próbuje przechwycić wyjątek. Kaboom to desktop jest jedyną opcją.

Wreszcie, program .NET robi coś nieproduktywnego ze stosem. Środowisko CLR zatwierdzi stos wątku. To drogie słowo, które oznacza, że ​​nie tylko rezerwuje rozmiar stosu, ale także zapewnia, że ​​miejsce jest zarezerwowane w pliku stronicowania systemu operacyjnego, dzięki czemu stos można zawsze zamienić w razie potrzeby. Niepowodzenie wykonania jest błędem krytycznym i bezwarunkowo kończy program. Dzieje się tak tylko na komputerze z bardzo małą pamięcią RAM, który uruchamia zbyt wiele procesów, taka maszyna zamieni się w melasę, zanim programy zaczną umierać. Możliwy problem 15+ lat temu, nie dzisiaj. Programiści, którzy dostrajają swój program, aby zachowywał się jak samochód wyścigowy F1, używają <disableCommitThreadStack>elementu w swoim pliku .config.

Fwiw, Cutler nie przestawał projektować systemów operacyjnych. To zdjęcie zostało zrobione podczas pracy na Azure.


Aktualizacja, zauważyłem, że .NET już nie zatwierdza stosu. Nie jestem pewien, kiedy i dlaczego to się stało, minęło zbyt dużo czasu, odkąd sprawdzałem. Domyślam się, że ta zmiana projektu nastąpiła gdzieś w okolicy .NET 4.5. Całkiem rozsądna zmiana.

Hans Passant
źródło
3
wrt do komentarza The only way to allocate space on the stack with C# is with the unsafe stackalloc keyword.- Czy zmienne lokalne, np. intzadeklarowane wewnątrz metody, nie są przechowywane na stosie? Myślę, że są.
RBT
2
Dobrze. Teraz zrozumiałem, że ramka stosu nie jest jedynym wyborem miejsca do przechowywania zmiennych lokalnych funkcji. Można go jednak przechowywać na ramie stosu, jak zasugerowano w jednym z punktorów. Bardzo pouczający Hans. Nie mogę powiedzieć wystarczająco dużo podziękowań za pisanie tak wnikliwych postów. Szczerze mówiąc, stos jest tak wielką abstrakcją dla programowania w ogóle, aby uniknąć niepotrzebnej złożoności.
RBT
Bardzo szczegółowy opis @ Hans. Właśnie się zastanawiałem, jaka jest minimalna możliwa wartość maxStackSizewątku? Nie mogłem go znaleźć w [MSDN] ( msdn.microsoft.com/en-us/library/5cykbwz4(v=vs.110).aspx ). Na podstawie twoich komentarzy wydaje się, że użycie stosu jest absolutnym minimum i mogę użyć najmniejszej wartości, aby pomieścić maksymalnie możliwe wątki. Dzięki.
MKR
1
@KFL: Możesz łatwo odpowiedzieć na swoje pytanie, próbując go!
Eric Lippert
1
Jeśli domyślnym zachowaniem jest zaprzestanie zatwierdzania stosu, ten plik znacznika
John Stewien,
5

Domyślny zarezerwowany rozmiar stosu jest określany przez konsolidator i może zostać nadpisany przez programistów poprzez zmianę wartości PE w czasie łączenia lub dla pojedynczego wątku poprzez określenie dwStackSizeparametru CreateThreadfunkcji WinAPI.

Jeśli utworzysz wątek z początkowym rozmiarem stosu większym lub równym domyślnemu rozmiarowi stosu, zostanie on zaokrąglony w górę do najbliższej wielokrotności 1 MB.

Dlaczego wartość jest równa 1 MB dla procesów 32-bitowych i 4 MB dla procesów 64-bitowych? Myślę, że powinieneś zapytać programistów, którzy zaprojektowali Windows, lub poczekać, aż ktoś z nich odpowie na twoje pytanie.

Zapewne Mark Russinovich o tym wie i możesz się z nim skontaktować . Być może znajdziesz te informacje w jego książkach Windows Internals sprzed szóstego wydania, które opisuje mniej informacji o stosach, a nie w swoim artykule . A może Raymond Chen zna powody, skoro pisze ciekawe rzeczy na temat wewnętrznych elementów systemu Windows i jego historii. On też może odpowiedzieć na twoje pytanie, ale powinieneś zamieścić sugestię w skrzynce sugestii .

Ale teraz spróbuję wyjaśnić kilka prawdopodobnych powodów, dla których Microsoft wybrał te wartości, korzystając z blogów MSDN, Marka i Raymonda.

Wartości domyślne mają te wartości prawdopodobnie dlatego, że w dawnych czasach komputery PC działały wolno, a alokowanie pamięci na stosie było znacznie szybsze niż przydzielanie pamięci w stercie. A ponieważ alokacje stosów były znacznie tańsze, były używane, ale wymagało to większego rozmiaru stosu.

Więc wartość była optymalnym zarezerwowanym rozmiarem stosu dla większości aplikacji. Jest optymalny, ponieważ pozwala na wykonywanie wielu zagnieżdżonych wywołań i alokację pamięci na stosie w celu przekazywania struktur do funkcji wywołujących. Jednocześnie pozwala na tworzenie wielu wątków.

W dzisiejszych czasach wartości te są najczęściej używane w celu zachowania kompatybilności wstecznej, ponieważ struktury, które są przekazywane jako parametry do funkcji WinAPI, są nadal alokowane na stosie. Ale jeśli nie używasz alokacji stosu, użycie stosu wątku będzie znacznie mniejsze niż domyślny 1 MB i jest to marnotrawstwo, jak wspomniał Hans Passant. Aby temu zapobiec, system operacyjny zatwierdza tylko pierwszą stronę stosu (4 KB), jeśli inna nie jest określona w nagłówku PE aplikacji. Inne strony są przydzielane na żądanie.

Niektóre aplikacje zastępują zarezerwowaną przestrzeń adresową i początkowo zobowiązały się do optymalizacji wykorzystania pamięci. Na przykład maksymalny rozmiar stosu wątku procesu natywnego usług IIS to 256 KB ( KB932909 ). A takie zmniejszenie wartości domyślnych jest zalecane przez Microsoft:

Najlepiej jest wybrać możliwie najmniejszy rozmiar stosu i zatwierdzić stos wymagany do niezawodnego działania nici lub włókna. Każda strona zarezerwowana dla stosu nie może być używana do żadnych innych celów.

Źródła:

  1. Rozmiar stosu wątków (Microsoft Docs)
  2. Przesuwanie granic systemu Windows: procesy i wątki (Mark Russinovich)
  3. Domyślnie maksymalny rozmiar stosu wątku utworzonego w natywnym procesie usług IIS to 256 KB (KB932909)
Yoh Deadfall
źródło
Jeśli chcę mieć większy rozmiar stosu, mogę go ustawić ( atalasoft.com/cs/blogs/rickm/archive/2008/04/22/… ). Chcę poznać względy i powody tej decyzji.
Nikolay Kostov
2
W porządku. Teraz cię rozumiem :) Domyślny rozmiar stosu powinien być optymalny (patrz komentarz @Lucas Trzesniewski) i powinien być zaokrąglony do najbliższej wielokrotności granulacji alokacji. Jeśli określony rozmiar stosu jest większy niż domyślny rozmiar stosu, zaokrągla się go w górę do najbliższej wielokrotności 1 MB. Dlatego Microsoft wybrał te rozmiary jako domyślne rozmiary stosu dla wszystkich aplikacji trybu użytkownika. I nie ma innych powodów.
Yoh Deadfall
Jakieś źródła? Jakaś dokumentacja? :)
Nikolay Kostov
@Yoh ciekawy link. Powinieneś podsumować to w swojej odpowiedzi.
Lucas Trzesniewski