¿Por qué el tamaño de la pila en C # es exactamente 1 MB?


102

Las PC de hoy tienen una gran cantidad de RAM física, pero aún así, el tamaño de pila de C # es de solo 1 MB para procesos de 32 bits y de 4 MB para procesos de 64 bits ( capacidad de pila en C # ).

¿Por qué el tamaño de la pila en CLR sigue siendo tan limitado?

¿Y por qué es exactamente 1 MB (4 MB) (y no 2 MB o 512 KB)? ¿Por qué se decidió utilizar estas cantidades?

Me interesan las consideraciones y las razones de esa decisión .


6
El tamaño de pila predeterminado para procesos de 64 bits es 4 MB, es de 1 MB para procesos de 32 bits. Usted puede modificar las principales roscas tamaño de la pila cambiando el valor en su cabecera PE. También puede especificar el tamaño de la pila utilizando la sobrecarga correcta del Threadconstructor. PERO, esto plantea la pregunta, ¿por qué necesita una pila más grande?
Yuval Itzchakov

2
Gracias, editado. :) La pregunta no es cómo usar un tamaño de pila más grande, sino por qué se decide que el tamaño de la pila sea de 1 MB (4 MB) .
Nikolay Kostov

8
Porque cada subproceso obtendrá este tamaño de pila de forma predeterminada, y la mayoría de los subprocesos no necesitan tanto. Acabo de arrancar mi PC y el sistema actualmente ejecuta 1200 subprocesos. Ahora haz los cálculos;)
Lucas Trzesniewski

2
@LucasTrzesniewski No solo eso, debe ser contagioso en la memoria . Tenga en cuenta que cuanto mayor sea el tamaño de la pila, menos subprocesos puede crear su proceso en su espacio de direcciones virtuales.
Yuval Itzchakov

No estoy seguro acerca de "exactamente" 1 MB: en mi Windows 8.1, una aplicación de consola .NET Core 3.1 tiene un 1572864tamaño de pila predeterminado en bytes (recuperado usando la API GetCurrentThreadStackLimits Win32). Puedo stackallocaproximadamente 1500000bytes sin StackOverflowException.
George Chakhidze

Respuestas:


210

ingrese la descripción de la imagen aquí

Estás mirando al tipo que tomó esa decisión. David Cutler y su equipo seleccionaron un megabyte como tamaño de pila predeterminado. Nada que ver con .NET o C #, esto se concretó cuando crearon Windows NT. Un megabyte es lo que elige cuando el encabezado EXE de un programa o la llamada de winapi CreateThread () no especifica explícitamente el tamaño de la pila. Que es la forma normal, casi cualquier programador deja que el sistema operativo elija el tamaño.

Esa elección probablemente sea anterior al diseño de Windows NT, la historia es demasiado turbia al respecto. Sería bueno que Cutler escribiera un libro al respecto, pero nunca ha sido escritor. Ha tenido una influencia extraordinaria en la forma en que funcionan las computadoras. Su primer diseño de sistema operativo fue RSX-11M, un sistema operativo de 16 bits para computadoras DEC (Digital Equipment Corporation). Influyó mucho en el CP / M de Gary Kildall, el primer sistema operativo decente para microprocesadores de 8 bits. Lo que influyó mucho en MS-DOS.

Su siguiente diseño fue VMS, un sistema operativo para procesadores de 32 bits con soporte de memoria virtual. Muy exitoso. El siguiente fue cancelado por DEC cuando la compañía comenzó a desintegrarse, sin poder competir con hardware de PC barato. Cue Microsoft, le hicieron una oferta que no pudo rechazar. Muchos de sus compañeros de trabajo también se unieron. Trabajaron en VMS v2, más conocido como Windows NT. DEC se molestó por eso, el dinero cambió de manos para liquidarlo. Si VMS ya eligió un megabyte es algo que no sé, solo conozco RSX-11 lo suficientemente bien. No es improbable.

Suficiente historia. Un megabyte es mucho , un hilo real rara vez consume más de un par de puñados de kilobytes. Entonces, un megabyte es en realidad un desperdicio. Sin embargo, es el tipo de desperdicio que puede permitirse en un sistema operativo de memoria virtual paginada a demanda, ese megabyte es solo memoria virtual . Solo números para el procesador, uno por cada 4096 bytes. En realidad, nunca usa la memoria física, la RAM en la máquina, hasta que realmente la dirige.

Es más excesivo en un programa .NET porque el tamaño de un megabyte se eligió originalmente para adaptarse a los programas nativos. Que tienden a crear grandes marcos de pila, almacenando cadenas y búferes (matrices) en la pila también. Infame por ser un vector de ataque de malware, un desbordamiento de búfer puede manipular el programa con datos. No es la forma en que funcionan los programas .NET, las cadenas y las matrices se asignan en el montón de GC y se comprueba la indexación. La única forma de asignar espacio en la pila con C # es con la palabra clave unsafe stackalloc .

El único uso no trivial de la pila en .NET es el jitter. Utiliza la pila de su hilo para compilar MSIL en código de máquina justo a tiempo. Nunca he visto ni comprobado cuánto espacio requiere, más bien depende de la naturaleza del código y de si el optimizador está habilitado o no, pero un par de decenas de kilobytes es una suposición aproximada. Que de otra manera es como este sitio web obtuvo su nombre, un desbordamiento de pila en un programa .NET es bastante fatal. No queda suficiente espacio (menos de 3 kilobytes) para seguir JIT de manera confiable con cualquier código que intente detectar la excepción. Kaboom al escritorio es la única opción.

Por último, pero no menos importante, un programa .NET hace algo bastante improductivo con la pila. El CLR confirmará la pila de un hilo. Esa es una palabra cara que significa que no solo reserva el tamaño de la pila, sino que también se asegura de que se reserve espacio en el archivo de paginación del sistema operativo para que la pila siempre pueda intercambiarse cuando sea necesario. No confirmar es un error fatal y termina un programa incondicionalmente. Eso solo sucede en una máquina con muy poca RAM que ejecuta demasiados procesos, tal máquina se habrá convertido en melaza antes de que los programas comiencen a morir. Un posible problema hace más de 15 años, no hoy. Los programadores que ajustan su programa para que actúe como un coche de carreras de F1 utilizan el <disableCommitThreadStack>elemento en su archivo .config.

Fwiw, Cutler no dejó de diseñar sistemas operativos. Esa foto se hizo mientras trabajaba en Azure.


Actualización, noté que .NET ya no confirma la pila. No estoy seguro de cuándo o por qué sucedió esto, ha pasado demasiado tiempo desde que lo comprobé. Supongo que este cambio de diseño ocurrió en algún lugar alrededor de .NET 4.5. Cambio bastante sensato.


3
wrt a su comentario The only way to allocate space on the stack with C# is with the unsafe stackalloc keyword.: ¿las variables locales, por ejemplo, una intdeclarada dentro de un método, no se almacenan en la pila? Creo que lo son.
RBT

2
Okay. Ahora entendí que un marco de pila no es la única opción de almacenamiento para las variables locales de una función. Sin embargo, se puede almacenar en un marco de pila, como se sugiere en uno de sus puntos. Hans muy esclarecedor. No puedo agradecerles lo suficiente por escribir publicaciones tan interesantes. Honestamente, la pila es una gran abstracción para la programación en general solo para evitar una complejidad innecesaria.
RBT

Descripción muy detallada @Hans. Me preguntaba cuál es el valor mínimo posible maxStackSizede un hilo. No pude encontrarlo en [MSDN] ( msdn.microsoft.com/en-us/library/5cykbwz4(v=vs.110).aspx ). Según sus comentarios, parece que el uso de la pila es mínimo absoluto y puedo usar el valor más pequeño para acomodar el máximo de subprocesos posibles. Gracias.
MKR

1
@KFL: ¡Podrías responder a tu pregunta fácilmente intentándolo!
Eric Lippert

1
Si el comportamiento predeterminado es dejar de confirmar la pila, entonces este archivo de rebajas debe editarse github.com/dotnet/docs/blob/master/docs/framework/…
John Stewien

5

El enlazador especifica el tamaño de pila reservado predeterminado y los desarrolladores pueden anularlo cambiando el valor de PE en el momento del enlace o para un subproceso individual especificando el dwStackSizeparámetro para la CreateThreadfunción WinAPI.

Si crea un subproceso con el tamaño de pila inicial mayor o igual que el tamaño de pila predeterminado, se redondea al múltiplo más cercano de 1 MB.

¿Por qué el valor es igual a 1 MB para procesos de 32 bits y 4 MB para 64 bits? Creo que debería preguntarle a los desarrolladores que diseñaron Windows o esperar hasta que alguno de ellos responda a su pregunta.

Probablemente Mark Russinovich lo sepa y puedas contactarlo . Tal vez pueda encontrar esta información en sus libros de Windows Internals anteriores a la sexta edición que describe menos información sobre las pilas en lugar de su artículo . O tal vez Raymond Chen conoce las razones ya que escribe cosas interesantes sobre los componentes internos de Windows y su historia. Él también puede responder tu pregunta, pero debes publicar una sugerencia en el Buzón de sugerencias .

Pero en este momento intentaré explicar algunas razones probables por las que Microsoft ha elegido estos valores utilizando los blogs de MSDN, Mark y Raymond.

Los valores predeterminados tienen estos valores probablemente porque en los primeros tiempos las PC eran lentas y la asignación de memoria en la pila era mucho más rápida que la asignación de memoria en la pila. Y dado que las asignaciones de pila eran mucho más baratas, se utilizaron, pero requirieron un tamaño de pila mayor.

Por tanto, el valor era el tamaño de pila reservado óptimo para la mayoría de las aplicaciones. Es óptimo porque permite hacer muchas llamadas anidadas y asignar memoria en la pila para pasar estructuras a funciones de llamada. Al mismo tiempo, permite crear muchos hilos.

Hoy en día, estos valores se utilizan principalmente para la compatibilidad con versiones anteriores, porque las estructuras que se pasan como parámetros a las funciones de WinAPI todavía se asignan en la pila. Pero si no está utilizando asignaciones de pila, el uso de pila de un hilo será significativamente menor que el 1 MB predeterminado y es un desperdicio, como mencionó Hans Passant. Y para evitar esto, el sistema operativo compromete solo la primera página de la pila (4 KB), si no se especifica otra en el encabezado PE de la aplicación. Otras páginas se asignan bajo demanda.

Algunas aplicaciones anulan el espacio de direcciones reservado e inicialmente se comprometieron a optimizar el uso de la memoria. Como ejemplo, el tamaño máximo de pila de un subproceso de un proceso nativo de IIS es 256 KB ( KB932909 ). Y Microsoft recomienda esta disminución de los valores predeterminados :

Es mejor elegir un tamaño de pila lo más pequeño posible y asignar la pila que se necesita para que el hilo o la fibra se ejecuten de forma fiable. Todas las páginas reservadas para la pila no se pueden utilizar para ningún otro propósito.

Fuentes:

  1. Tamaño de la pila de subprocesos (Microsoft Docs)
  2. Empujando los límites de Windows: procesos e hilos (Mark Russinovich)
  3. De forma predeterminada, el tamaño máximo de pila de un subproceso que se crea en un proceso IIS nativo es de 256 KB (KB932909)

Si quiero un tamaño de pila más grande, puedo configurarlo ( atalasoft.com/cs/blogs/rickm/archive/2008/04/22/… ). Quiero conocer las consideraciones y razones detrás de esa decisión.
Nikolay Kostov

2
Bueno. Ahora te entiendo :) El tamaño de pila predeterminado debe ser óptimo (ver el comentario de @Lucas Trzesniewski) y debe redondearse al múltiplo más cercano de la granularidad de la asignación. Si el tamaño de pila especificado es mayor que el tamaño de pila predeterminado, se redondea al múltiplo más cercano de 1 MB. Por lo tanto, Microsoft eligió estos tamaños como tamaños de pila predeterminados para todas las aplicaciones de modo de usuario. Y no hay otras razones.
Yoh Deadfall

¿Alguna fuente? ¿Alguna documentación? :)
Nikolay Kostov

@Yoh enlace interesante. Deberías resumir eso en tu respuesta.
Lucas Trzesniewski
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.