¿Por qué el tamaño de la memoria de pila es tan limitado?

81

Cuando asigna memoria en el montón, el único límite es RAM libre (o memoria virtual). Genera Gb de memoria.

Entonces, ¿por qué el tamaño de la pila es tan limitado (alrededor de 1 Mb)? ¿Qué razón técnica le impide crear objetos realmente grandes en la pila?

Actualización : es posible que mi intención no sea clara, no quiero asignar objetos grandes en la pila y no necesito una pila más grande. Esta pregunta es pura curiosidad.

ondulado
fuente
¿Por qué sería práctico crear objetos grandes en el montón? (Las cadenas de llamadas suelen ir a la pila.)
Makoto
4
Creo que la respuesta real es más simple de lo que muestran la mayoría de las respuestas: "porque así es como siempre lo hemos hecho, y hasta ahora ha ido bien, ¿por qué cambiar?"
Jerry Coffin
@JerryCoffin ¿Ha leído alguna de las respuestas publicadas hasta ahora? Hay más información sobre esta pregunta.
user1202136
3
@ user1202136: Los he leído todos, pero la gente está adivinando, y mi suposición es que muchos de los factores que están citando probablemente ni siquiera fueron considerados al tomar las decisiones originales sobre el tema. Para acuñar una frase, "a veces un puro es sólo un puro".
Jerry Coffin
3
"¿Qué tan grande deberíamos hacer la pila predeterminada?" "Oh, no sé, ¿cuántos hilos podemos ejecutar?" "Explota en algún lugar sobre una K" "Está bien, entonces, lo llamaremos 2K, tenemos 2 Gig de virtual, así que ¿qué tal 1 meg?" "Sí, está bien, ¿cuál es el próximo problema?"
Martin James

Respuestas:

45

Mi intuición es la siguiente. La pila no es tan fácil de administrar como el montón. La pila debe almacenarse en ubicaciones de memoria continua. Esto significa que no puede asignar aleatoriamente la pila según sea necesario, pero necesita al menos reservar direcciones virtuales para ese propósito. Cuanto mayor sea el tamaño del espacio de direcciones virtuales reservado, menos subprocesos puede crear.

Por ejemplo, una aplicación de 32 bits generalmente tiene un espacio de direcciones virtual de 2 GB. Esto significa que si el tamaño de la pila es de 2 MB (por defecto en pthreads), puede crear un máximo de 1024 hilos. Esto puede ser pequeño para aplicaciones como servidores web. Aumentar el tamaño de la pila a, digamos, 100 MB (es decir, reserva 100 MB, pero no necesariamente asigna 100 MB a la pila inmediatamente), limitaría el número de subprocesos a aproximadamente 20, lo que puede ser limitante incluso para aplicaciones GUI simples.

Una pregunta interesante es, ¿por qué todavía tenemos este límite en plataformas de 64 bits? No sé la respuesta, pero supongo que la gente ya está acostumbrada a algunas "mejores prácticas de pila": tenga cuidado de asignar objetos enormes en la pila y, si es necesario, aumente manualmente el tamaño de la pila. Por lo tanto, a nadie le resultó útil agregar soporte de pila "enorme" en plataformas de 64 bits.

user1202136
fuente
Muchas máquinas de 64 bits tienen solo direcciones de 48 bits (otorgan una gran ganancia sobre 32 bits, pero aún limitada). Incluso con espacio adicional, tiene que preocuparse por cómo la reserva con respecto a las tablas de páginas, es decir, siempre hay gastos generales en tener más espacio. Probablemente sea igual de económico, si no más económico, asignar un nuevo segmento (mmap) en lugar de reservar grandes espacios de pila para cada hilo.
edA-qa mort-ora-y
4
@ edA-qamort-ora-y: Esta respuesta no se refiere a la asignación , sino a la reserva de memoria virtual , que es casi gratuita y ciertamente mucho más rápida que mmap.
Mooing Duck
33

Un aspecto que nadie ha mencionado todavía:

Un tamaño de pila limitado es un mecanismo de detección y contención de errores.

Generalmente, el trabajo principal de la pila en C y C ++ es realizar un seguimiento de la pila de llamadas y las variables locales, y si la pila crece fuera de los límites, casi siempre es un error en el diseño y / o el comportamiento de la aplicación. .

Si se permitiera que la pila creciera arbitrariamente, estos errores (como la recursividad infinita) se detectarían muy tarde, solo después de que se agoten los recursos del sistema operativo. Esto se evita estableciendo un límite arbitrario para el tamaño de la pila. El tamaño real no es tan importante, aparte de que es lo suficientemente pequeño como para evitar la degradación del sistema.

Andreas Grapentin
fuente
Es posible que tenga un problema similar con los objetos asignados (ya que una forma de reemplazar la recursividad es manejar una pila manualmente). Esa limitación obliga a usar otras formas (que no son necesariamente más seguras / más simples / ..) (Tenga en cuenta el número de comentarios sobre la implementación de la lista (juguete) std::unique_ptrpara escribir un destructor (y no depender del puntero inteligente)).
Jarod42
15

Es solo un tamaño predeterminado. Si necesita más, puede obtener más, generalmente diciéndole al vinculador que asigne espacio de pila adicional.

La desventaja de tener pilas grandes es que si crea muchos subprocesos, necesitarán una pila cada uno. Si todas las pilas asignan varios MB, pero no los utilizan, se desperdiciará espacio.

Tienes que encontrar el equilibrio adecuado para tu programa.


Algunas personas, como @BJovke, creen que la memoria virtual es esencialmente gratuita. Es cierto que no es necesario tener una memoria física que respalde toda la memoria virtual. Tienes que poder al menos dar direcciones a la memoria virtual.

Sin embargo, en una PC típica de 32 bits, el tamaño de la memoria virtual es el mismo que el tamaño de la memoria física, porque solo tenemos 32 bits para cualquier dirección, virtual o no.

Debido a que todos los subprocesos de un proceso comparten el mismo espacio de direcciones, deben dividirlo entre ellos. Y después de que el sistema operativo ha tomado su parte, quedan "sólo" 2-3 GB para una aplicación. Y ese tamaño es el límite tanto para la memoria física como para la virtual, porque simplemente no hay más direcciones.

Bo Persson
fuente
El mayor problema de los subprocesos es que no puede señalar fácilmente los objetos de la pila a otros subprocesos. O el hilo productor tiene que esperar sincrónicamente a que el hilo consumidor libere el objeto o se deben realizar copias profundas costosas y generadoras de contención.
Martin James
2
@MartinJames: Nadie está diciendo que todos los objetos deberían estar en la pila, estamos discutiendo por qué el tamaño de pila predeterminado es pequeño.
Mooing Duck
No se desperdiciará espacio, el tamaño de la pila es solo una reserva de espacio de direcciones virtuales continuo. Entonces, si establece un tamaño de pila de 100 MB, la cantidad de RAM que realmente se utilizará depende del consumo de pila en subprocesos.
BJovke
1
@BJovke - Pero el espacio de direcciones virtuales aún se agotará . En un proceso de 32 bits, esto está limitado a unos pocos GB, por lo que solo reservar 20 * 100 MB le causará problemas.
Bo Persson
7

Por un lado, la pila es continua, por lo que si asigna 12 MB, debe eliminar 12 MB cuando desee ir por debajo de lo que creó. Además, mover objetos se vuelve mucho más difícil. A continuación, se muestra un ejemplo del mundo real que puede facilitar la comprensión de las cosas:

Digamos que está apilando cajas alrededor de una habitación. Cuál es más fácil de administrar:

  • apilar cajas de cualquier peso una encima de la otra, pero cuando necesite poner algo en la parte inferior, debe deshacer toda la pila. Si desea sacar un artículo de la pila y dárselo a otra persona, debe quitar todas las cajas y mover la caja a la pila de la otra persona (solo pila)
  • Pones todas tus cajas (excepto las cajas realmente pequeñas) en un área especial donde no apilas cosas encima de otras cosas y escribes donde las pones en una hoja de papel (un puntero) y colocas el papel la pila. Si necesita darle la caja a otra persona, simplemente entréguele la hoja de papel de su pila, o simplemente déle una fotocopia del papel y deje el original donde estaba en su pila. (Pila + montón)

Esos dos ejemplos son generalizaciones burdas y hay algunos puntos que son descaradamente erróneos en la analogía, pero está lo suficientemente cerca como para ayudarlo a ver las ventajas en ambos casos.

Scott Chamberlain
fuente
@MooingDuck Sí, pero está trabajando en la memoria virtual en su programa, si entro en una subrutina, pongo algo en la pila, luego regreso de la subrutina, tendré que desasignar o mover el objeto que creé antes de poder desconectar la pila para volver a donde vengo.
Scott Chamberlain
1
aunque mi comentario se debió a una mala interpretación (y lo eliminé), todavía no estoy de acuerdo con esta respuesta. Eliminar 12 MB de la parte superior de la pila es literalmente un código de operación. Es básicamente gratis. Además, los compiladores pueden engañar a la regla de "pila", así que no, no tienen que copiar / mover el objeto antes de desenrollarlo para devolverlo. Así que creo que tu comentario también es incorrecto.
Mooing Duck
Bueno, por lo general no importa mucho que la desasignación de 12 MB requiera un código de operación en la pila y más de 100 en el montón; probablemente esté por debajo del nivel de ruido de procesar el búfer de 12 MB. Si los compiladores quieren hacer trampa cuando notan que se está devolviendo un objeto ridículamente grande (por ejemplo, moviendo el SP antes de la llamada para hacer que el espacio de objetos forme parte de la pila de llamadas), entonces está bien, TBH, los desarrolladores que devuelven tales los objetos, (en lugar de punteros / referencias), son un tanto desafiados por la programación.
Martin James
@MartinJames: La especificación de C ++ también dice que la función generalmente puede colocar los datos directamente en el búfer de destino y no usar el temporal, por lo que si tiene cuidado, no hay sobrecarga para devolver un búfer de 12 MB por valor.
Mooing Duck
3

Piense en la pila en el orden de cerca a lejos. Los registros están cerca de la CPU (rápido), la pila está un poco más lejos (pero aún relativamente cerca) y el montón está lejos (acceso lento).

La pila vive en el montón, por supuesto, pero aún así, dado que se usa continuamente, probablemente nunca abandone la (s) caché (s) de la CPU, lo que lo hace más rápido que el acceso al montón promedio. Ésta es una razón para mantener la pila de un tamaño razonable; para mantenerlo en caché tanto como sea posible. Asignar objetos de pila grande (posiblemente cambiar el tamaño de la pila automáticamente a medida que se desbordan) va en contra de este principio.

Así que es un buen paradigma para el rendimiento, no solo un remanente de los viejos tiempos.

Ruud van Gaal
fuente
4
Si bien creo que el almacenamiento en caché juega un papel importante en la razón de reducir artificialmente el tamaño de la pila, debo corregirlo en la afirmación "la pila vive en el montón". Tanto la pila como el montón viven en la memoria (virtual o físicamente).
Sebastián Hojas
¿Cómo se relaciona "cerca o lejos" con la velocidad de acceso?
Minh Nghĩa
@ MinhNghĩa Bueno, las variables en RAM se almacenan en caché en la memoria L2, luego se almacenan en caché en la memoria L1, e incluso esas se almacenan en caché en los registros. El acceso a la RAM es lento, a L2 es más rápido, L1 es aún más rápido y el registro es más rápido. Lo que creo que OP quiso decir es que se supone que se debe acceder rápidamente a las variables almacenadas en la pila, por lo que la CPU hará todo lo posible para mantener las variables de la pila cerca de ella, por lo tanto, debe hacerlo pequeño, por lo tanto, la CPU puede acceder a las variables más rápido.
157 239n
1

Muchas de las cosas para las que crees que necesitas una gran pila se pueden hacer de otra manera.

Los "Algoritmos" de Sedgewick tienen un par de buenos ejemplos de "eliminar" la recursividad de algoritmos recursivos como QuickSort, reemplazando la recursividad con iteración. En realidad, el algoritmo sigue siendo recursivo y todavía hay una pila, pero usted asigna la pila de clasificación en el montón, en lugar de utilizar la pila en tiempo de ejecución.

(Estoy a favor de la segunda edición, con algoritmos dados en Pascal. Se puede usar por ocho dólares).

Otra forma de verlo es que si cree que necesita una pila grande, su código es ineficiente. Hay una forma mejor que usa menos pila.

Mike Crawford
fuente
-8

No creo que haya ninguna razón técnica, pero sería una aplicación extraña que acaba de crear un gran superobjeto en la pila. Los objetos apilados carecen de flexibilidad, lo que se vuelve más problemático con el aumento de tamaño: no puede regresar sin destruirlos y no puede ponerlos en cola en otros subprocesos.

Martin James
fuente
1
Nadie está diciendo que todos los objetos deberían estar en la pila, estamos discutiendo por qué el tamaño de pila predeterminado es pequeño.
Mooing Duck
¡No es pequeño! ¿Cuántas llamadas de función tendrías que atravesar para utilizar 1 MB de pila? De todos modos, los valores predeterminados se cambian fácilmente en el enlazador y, por lo tanto, nos quedamos con '¿por qué usar pila en lugar de montón?'
Martin James
3
una llamada de función. int main() { char buffer[1048576]; } Es un problema de novato muy común. Seguro que hay una solución alternativa fácil, pero ¿por qué deberíamos tener que solucionar el tamaño de la pila?
Mooing Duck
Bueno, por un lado, no querría los 12 MB, (o de hecho, 1 MB) de requisito de pila infligido en la pila de cada hilo que llama a la función afectada. Dicho esto, estoy de acuerdo en que 1 MB es un poco tacaño. Estaría feliz con un valor predeterminado de 100 MB, después de todo, no hay nada que me impida bajarlo a 128K de la misma manera que no hay nada que impida que otros desarrolladores lo suban.
Martin James
1
¿Por qué no querrías infligir 12 MB de pila en tu hilo? La única razón de esto es que las pilas son pequeñas. Ese es un argumento recursivo.
Mooing Duck