Warum beträgt die Stapelgröße in C # genau 1 MB?

102

Heutige PCs verfügen über eine große Menge an physischem RAM, die Stapelgröße von C # beträgt jedoch nur 1 MB für 32-Bit-Prozesse und 4 MB für 64-Bit-Prozesse ( Stapelkapazität in C # ).

Warum ist die Stapelgröße in CLR immer noch so begrenzt?

Und warum ist es genau 1 MB (4 MB) (und nicht 2 MB oder 512 KB)? Warum wurde beschlossen, diese Beträge zu verwenden?

Ich interessiere mich für Überlegungen und Gründe hinter dieser Entscheidung .

Nikolay Kostov
quelle
6
Die Standardstapelgröße für 64-Bit-Prozesse beträgt 4 MB, für 32-Bit-Prozesse 1 MB. Sie können die Stapelgröße des Hauptthreads ändern, indem Sie den Wert in seinem PE-Header ändern. Sie können die Stapelgröße auch angeben, indem Sie die richtige Überladung des ThreadKonstruktors verwenden. ABER das wirft die Frage auf, warum Sie einen größeren Stapel benötigen.
Yuval Itzchakov
2
Danke, bearbeitet. :) Die Frage ist nicht, wie man einen größeren Stapel verwendet, sondern warum die Stapelgröße auf 1 MB (4 MB) festgelegt wird .
Nikolay Kostov
8
Weil jeder Thread standardmäßig diese Stapelgröße erhält und die meisten Threads nicht so viel benötigen. Ich habe gerade meinen PC hochgefahren und das System führt derzeit 1200 Threads aus. Jetzt rechnen Sie nach;)
Lucas Trzesniewski
2
@LucasTrzesniewski Nicht nur das, es muss im Gedächtnis ansteckend sein . Beachten Sie, je größer der Stapel ist, desto weniger Threads kann Ihr Prozess in seinem virtuellen Adressraum erstellen.
Yuval Itzchakov
Nicht sicher über "genau" 1 MB: Unter Windows 8.1 hat eine .NET Core 3.1-Konsolenanwendung die 1572864Standardstapelgröße von Bytes (Abgerufen mit der Win32-API GetCurrentThreadStackLimits). Ich kann stackallocungefähr 1500000Bytes ohne StackOverflowException.
George Chakhidze

Antworten:

210

Geben Sie hier die Bildbeschreibung ein

Sie sehen den Mann, der diese Wahl getroffen hat. David Cutler und sein Team haben ein Megabyte als Standardstapelgröße ausgewählt. Nichts mit .NET oder C # zu tun, dies wurde beim Erstellen von Windows NT festgenagelt. Ein Megabyte wird ausgewählt, wenn der EXE-Header eines Programms oder der Winapi-Aufruf CreateThread () die Stapelgröße nicht explizit angibt. Das ist der normale Weg, fast jeder Programmierer überlässt es dem Betriebssystem, die Größe auszuwählen.

Diese Wahl datiert wahrscheinlich vor dem Windows NT-Design, die Geschichte ist diesbezüglich viel zu trübe. Wäre schön, wenn Cutler ein Buch darüber schreiben würde, aber er war nie Schriftsteller. Er hat die Arbeitsweise von Computern außerordentlich beeinflusst. Sein erstes Betriebssystem war RSX-11M, ein 16-Bit-Betriebssystem für DEC-Computer (Digital Equipment Corporation). Es hat Gary Kildalls CP / M, das erste anständige Betriebssystem für 8-Bit-Mikroprozessoren, stark beeinflusst. Was MS-DOS stark beeinflusst hat.

Sein nächstes Design war VMS, ein Betriebssystem für 32-Bit-Prozessoren mit Unterstützung für virtuellen Speicher. Sehr erfolgreich. Sein nächstes wurde von DEC abgesagt, als das Unternehmen anfing, sich aufzulösen, da es nicht in der Lage war, mit billiger PC-Hardware zu konkurrieren. Cue Microsoft, sie machten ihm ein Angebot, das er nicht ablehnen konnte. Viele seiner Mitarbeiter schlossen sich ebenfalls an. Sie arbeiteten an VMS v2, besser bekannt als Windows NT. DEC war darüber verärgert, Geld wechselte den Besitzer, um es zu regeln. Ob VMS bereits ein Megabyte ausgewählt hat, weiß ich nicht, ich kenne RSX-11 nur gut genug. Das ist nicht unwahrscheinlich.

Genug Geschichte. Ein Megabyte ist viel , ein echter Thread verbraucht selten mehr als ein paar Handvoll Kilobyte. Ein Megabyte ist also eigentlich ziemlich verschwenderisch. Es ist jedoch die Art von Verschwendung, die Sie sich bei einem bedarfsgesteuerten Betriebssystem für virtuellen Speicher leisten können, dass Megabyte nur virtueller Speicher ist . Nur Zahlen an den Prozessor, jeweils eine pro 4096 Bytes. Sie verwenden niemals den physischen Speicher, den RAM in der Maschine, bis Sie ihn tatsächlich ansprechen.

In einem .NET-Programm ist dies besonders übertrieben, da die Größe von einem Megabyte ursprünglich für native Programme ausgewählt wurde. Diese neigen dazu, große Stapelrahmen zu erstellen und Zeichenfolgen und Puffer (Arrays) ebenfalls auf dem Stapel zu speichern. Ein Pufferüberlauf, der als Malware-Angriffsvektor berüchtigt ist, kann das Programm mit Daten manipulieren. Nicht die Art und Weise, wie .NET-Programme funktionieren, Zeichenfolgen und Arrays werden auf dem GC-Heap zugewiesen und die Indizierung wird überprüft. Die einzige Möglichkeit, mit C # Speicherplatz auf dem Stapel zuzuweisen, ist das Schlüsselwort unsicheres stackalloc .

Die einzige nicht triviale Verwendung des Stacks in .NET erfolgt durch den Jitter. Es verwendet den Stapel Ihres Threads, um MSIL just-in-time zu Maschinencode zu kompilieren. Ich habe noch nie gesehen oder überprüft, wie viel Speicherplatz benötigt wird. Es hängt vielmehr von der Art des Codes ab und davon, ob der Optimierer aktiviert ist oder nicht, aber ein paar zehn Kilobyte sind eine grobe Vermutung. Ansonsten hat diese Website ihren Namen erhalten. Ein Stapelüberlauf in einem .NET-Programm ist ziemlich fatal. Es ist nicht mehr genügend Speicherplatz vorhanden (weniger als 3 Kilobyte), um noch zuverlässig Code zu JITEN, der versucht, die Ausnahme abzufangen. Kaboom to Desktop ist die einzige Option.

Last but not least macht ein .NET-Programm etwas ziemlich Unproduktives mit dem Stack. Die CLR schreibt den Stapel eines Threads fest. Das ist ein teures Wort, das bedeutet, dass nicht nur die Größe des Stapels reserviert wird, sondern auch sichergestellt wird, dass Speicherplatz in der Auslagerungsdatei des Betriebssystems reserviert ist, sodass der Stapel bei Bedarf jederzeit ausgetauscht werden kann. Das Nicht-Festschreiben ist ein schwerwiegender Fehler und beendet ein Programm bedingungslos. Dies geschieht nur auf einem Computer mit sehr wenig RAM, auf dem viel zu viele Prozesse ausgeführt werden. Ein solcher Computer hat sich in Melasse verwandelt, bevor Programme zu sterben beginnen. Ein mögliches Problem vor mehr als 15 Jahren, nicht heute. Programmierer, die ihr Programm so <disableCommitThreadStack>einstellen, dass es sich wie ein F1-Rennwagen verhält , verwenden das Element in ihrer .config-Datei.

Fwiw, Cutler hat nicht aufgehört, Betriebssysteme zu entwerfen. Dieses Foto wurde gemacht, während er an Azure arbeitete.


Update, ich habe festgestellt, dass .NET den Stack nicht mehr festschreibt. Ich bin mir nicht ganz sicher, wann oder warum dies passiert ist. Es ist zu lange her, seit ich es überprüft habe. Ich vermute, dass diese Designänderung irgendwo um .NET 4.5 herum stattgefunden hat. Ziemlich vernünftige Veränderung.

Hans Passant
quelle
3
Zu Ihrem Kommentar The only way to allocate space on the stack with C# is with the unsafe stackalloc keyword.- Werden lokale Variablen, z. B. eine intin einer Methode deklarierte, nicht im Stapel gespeichert? Ich glaube sie sind.
RBT
2
OK. Jetzt habe ich festgestellt, dass ein Stack-Frame nicht die einzige Wahl für die Speicherung lokaler Variablen einer Funktion ist. Es kann jedoch auf einem Stapelrahmen gespeichert werden, wie in einem Ihrer Aufzählungspunkte vorgeschlagen. Sehr aufschlussreicher Hans. Ich kann nicht genug von Danke sagen, dass Sie so aufschlussreiche Beiträge geschrieben haben. Ehrlich gesagt ist Stack eine so große Abstraktion für die Programmierung im Allgemeinen, nur um unnötige Komplexität zu vermeiden.
RBT
Sehr detaillierte Beschreibung @Hans. Ich habe mich nur gefragt, was der minimal mögliche Wert für maxStackSizeeinen Thread ist. Ich konnte es auf [MSDN] ( msdn.microsoft.com/en-us/library/5cykbwz4(v=vs.110).aspx ) nicht finden . Basierend auf Ihren Kommentaren scheint die Stapelverwendung absolut minimal zu sein, und ich kann den kleinsten Wert verwenden, um maximal mögliche Threads aufzunehmen. Vielen Dank.
MKR
1
@KFL: Sie könnten Ihre Frage leicht beantworten, indem Sie es versuchen!
Eric Lippert
1
Wenn das Standardverhalten darin besteht, den Stapel nicht mehr
festzuschreiben
5

Die standardmäßig reservierte Stapelgröße wird vom Linker angegeben und kann von Entwicklern durch Ändern des PE-Werts zur Verknüpfungszeit oder für einen einzelnen Thread durch Angabe des dwStackSizeParameters für die CreateThreadWinAPI-Funktion überschrieben werden .

Wenn Sie einen Thread erstellen, dessen anfängliche Stapelgröße größer oder gleich der Standardstapelgröße ist, wird er auf das nächste Vielfache von 1 MB aufgerundet.

Warum entspricht der Wert 1 MB für 32-Bit-Prozesse und 4 MB für 64-Bit? Ich denke, Sie sollten Entwickler fragen, die Windows entworfen haben, oder warten, bis jemand von ihnen Ihre Frage beantwortet.

Wahrscheinlich weiß Mark Russinovich das und Sie können ihn kontaktieren . Vielleicht finden Sie diese Informationen in seinen Windows Internals-Büchern vor der sechsten Ausgabe, in denen weniger Informationen zu Stacks als zu seinem Artikel beschrieben werden . Oder vielleicht kennt Raymond Chen Gründe, da er interessante Dinge über Windows-Interna und deren Geschichte schreibt. Er kann Ihre Frage auch beantworten, aber Sie sollten einen Vorschlag in das Vorschlagsfeld stellen .

Aber zu diesem Zeitpunkt werde ich versuchen, einige wahrscheinliche Gründe zu erklären, warum Microsoft diese Werte mithilfe von Blogs von MSDN, Mark und Raymond ausgewählt hat.

Die Standardeinstellungen haben diese Werte wahrscheinlich, weil PCs in früheren Zeiten langsam waren und die Zuweisung von Speicher auf dem Stapel viel schneller war als die Zuweisung von Speicher auf dem Heap. Und da Stapelzuweisungen viel billiger waren, wurden sie verwendet, aber es erforderte eine größere Stapelgröße.

Der Wert war also die optimale reservierte Stapelgröße für die meisten Anwendungen. Dies ist optimal, da viele verschachtelte Aufrufe ausgeführt und Speicher auf dem Stapel zugewiesen werden können, um Strukturen an aufrufende Funktionen zu übergeben. Gleichzeitig können viele Threads erstellt werden.

Heutzutage werden diese Werte hauptsächlich aus Gründen der Abwärtskompatibilität verwendet, da Strukturen, die als Parameter an WinAPI-Funktionen übergeben werden, weiterhin auf dem Stapel zugewiesen sind. Wenn Sie jedoch keine Stapelzuweisungen verwenden, ist die Stapelauslastung eines Threads erheblich geringer als die Standard-1 MB und es ist verschwenderisch, wie Hans Passant erwähnt hat. Um dies zu verhindern, schreibt das Betriebssystem nur die erste Seite des Stapels (4 KB) fest, wenn im PE-Header der Anwendung keine andere angegeben ist. Andere Seiten werden auf Anfrage zugewiesen.

Einige Anwendungen überschreiben den reservierten Adressraum und verpflichten sich zunächst, die Speichernutzung zu optimieren. Die maximale Stapelgröße des Threads eines nativen IIS-Prozesses beträgt beispielsweise 256 KB ( KB932909 ). Und diese Verringerung der Standardwerte wird von Microsoft empfohlen :

Es ist am besten, eine möglichst kleine Stapelgröße zu wählen und den Stapel festzuschreiben, der für einen zuverlässigen Betrieb des Threads oder der Faser erforderlich ist. Jede Seite, die für den Stapel reserviert ist, kann nicht für andere Zwecke verwendet werden.

Quellen:

  1. Thread-Stapelgröße (Microsoft Docs)
  2. Die Grenzen von Windows überschreiten: Prozesse und Themen (Mark Russinovich)
  3. Standardmäßig beträgt die maximale Stapelgröße eines Threads, der in einem nativen IIS-Prozess erstellt wird, 256 KB (KB932909).
Yoh Deadfall
quelle
Wenn ich einen größeren Stapel möchte, kann ich ihn einstellen ( atalasoft.com/cs/blogs/rickm/archive/2008/04/22/… ). Ich möchte die Überlegungen und Gründe für diese Entscheidung kennen.
Nikolay Kostov
2
In Ordnung. Jetzt verstehe ich Sie :) Die Standardstapelgröße sollte optimal sein (siehe Kommentar von @Lucas Trzesniewski) und auf das nächste Vielfache der Zuordnungsgranularität gerundet werden. Wenn die angegebene Stapelgröße größer als die Standardstapelgröße ist, wird sie auf das nächste Vielfache von 1 MB aufgerundet. Daher hat Microsoft diese Größen als Standardstapelgrößen für alle Anwendungen im Benutzermodus ausgewählt. Und es gibt keine anderen Gründe.
Yoh Deadfall
Irgendwelche Quellen? Irgendeine Dokumentation? :)
Nikolay Kostov
@ Yoh interessanter Link. Sie sollten das in Ihrer Antwort zusammenfassen.
Lucas Trzesniewski