tamaño pilas pila maximo informatica estructura ejemplos datos colas arreglo aplicaciones c++ c memory stack

pilas - ¿Cómo C y C++ almacenan objetos grandes en la pila?



pilas y colas en c++ (11)

¿Cómo defines un objeto grande? ¿Estamos hablando de mayor o menor que el tamaño del espacio de pila asignado?

por ejemplo, si tienes algo como esto:

void main() { int reallyreallybigobjectonthestack[1000000000]; }

Dependiendo de su sistema, es probable que obtenga una segfault porque simplemente no hay suficiente espacio para almacenar el objeto. De lo contrario, se almacena como cualquier otro objeto. Si está hablando en la memoria física real, entonces no tiene que preocuparse por esto porque la memoria virtual en el nivel del sistema operativo se encargará de manejar eso.

Además, el tamaño de la pila probablemente no sea del tamaño de un entero; depende totalmente de su sistema operativo y del diseño de las aplicaciones. Espacio de direcciones virtuales .

Estoy tratando de descubrir cómo C y C ++ almacenan objetos grandes en la pila. Por lo general, la pila es del tamaño de un entero, por lo que no entiendo cómo se almacenan allí los objetos más grandes. ¿Simplemente ocupan múltiples "ranuras" de pila?


Cada vez que ingresa una función, la pila crece para ajustarse a las variables locales en esa función. Dada una clase largeObject que usa digamos 400 bytes:

void MyFunc(int p1, largeObject p2, largeObject *p3) { int s1; largeObject s2; largeObject *s3; }

Cuando llamas a esta función, tu pila se verá más o menos así (los detalles variarán según la convención y la arquitectura de llamada):

[... rest of stack ...] [4 bytes for p1] [400 bytes for p2] [4 bytes for p3] [return address] [old frame pointer] [4 bytes for s1] [400 bytes for s2] [4 bytes for s3]

Consulte Convenciones de llamadas x86 para obtener información sobre cómo funciona la pila. MSDN también tiene algunos diagramas agradables para algunas convecciones de llamadas diferentes, con código de muestra y diagramas de pila resultantes .


Como han dicho otros, no está claro solo lo que quiere decir con "objetos grandes" ... Sin embargo, ya que usted pregunta

¿Simplemente ocupan múltiples "ranuras" de pila?

Voy a suponer que simplemente significa algo más grande que un número entero. Sin embargo, como señaló otra persona, la pila no tiene "ranuras" de tamaño entero; es solo una sección de la memoria, y cada byte tiene su propia dirección. El compilador rastrea cada variable por la dirección del primer byte de esa variable; este es el valor que obtienes si usas el operador de dirección ( &var ), y el valor de un puntero es solo esta dirección para alguna otra variable . El compilador también sabe de qué tipo es cada variable (lo dijiste cuando declaraste la variable), y sabe qué tamaño debe tener cada tipo: cuando compilas el programa, hace lo que sea necesario para calcular cuánto espacio las variables necesitarán cuando se llama a una función, e incluye el resultado de eso en el código de punto de entrada de función (el marco de pila que mencionó PDaddy).


El tamaño de la pila es limitado. Por lo general, el tamaño de la pila se establece cuando se crea el proceso. Cada subproceso de ese proceso obtiene automáticamente el tamaño de pila predeterminado si no se especifica lo contrario en la llamada CreateThread (). Entonces, sí: puede haber múltiples ''ranuras'' de pila, pero cada subproceso solo tiene una. Y no se pueden compartir entre hilos.

Si colocas objetos que son más grandes que el tamaño restante de la pila en la pila, obtendrás un desbordamiento de pila y tu aplicación se bloqueará.

Entonces, si tiene objetos muy grandes, colóquelos en el montón, no en la pila. El montón solo está limitado por la cantidad de memoria virtual (que es una magnitud mayor que la pila).


En C y C ++ no deberías almacenar objetos grandes en la pila, porque la pila está limitada (como habrás adivinado). La pila para cada subproceso suele ser de solo un par de megabytes o menos (se puede especificar al crear un subproceso). Cuando llamas "nuevo" para crear un objeto, no se pone en la pila, sino que se pone en el montón.


Puede tener objetos lo suficientemente grandes (o lo suficientemente numerosos) que no tenga sentido colocarlos en la pila. En ese caso, puede colocar el objeto en el montón y colocarle un puntero en la pila. Esta es una diferencia entre pasar por valor y pasar por referencia.


Push instrucciones Push y pop usualmente no se usan para almacenar variables de marco de pila local. Al comienzo de la función, el marco de la pila se configura decrementando el puntero de la pila por el número de bytes (alineados con el tamaño de la palabra) requerido por las variables locales de la función. Esto asigna la cantidad de espacio requerida "en la pila" para estos valores. A continuación, se accede a todas las variables locales mediante un puntero a este marco de pila ( ebp en x86).


La pila es un gran bloque de memoria que almacena variables locales, información para regresar de llamadas a funciones, etc. El tamaño real de la pila varía significativamente en el sistema operativo. Por ejemplo, al crear un nuevo hilo en Windows, el tamaño predeterminado es 1 MB .

Si intentas crear un objeto de pila que necesita más memoria de la que está disponible actualmente en la pila, obtienes un desbordamiento de pila y ocurren cosas malas. Una gran clase de código de explotación intenta deliberadamente crear estas o condiciones similares.

La pila no está dividida en trozos de tamaño entero. Es solo una matriz plana de bytes. Está indexado por un "entero" de tipo size_t (no int). Si crea un objeto de pila grande que encaje en el espacio disponible actualmente, simplemente usa ese espacio subiendo (o bajándose) el puntero de pila.

Como otros han señalado, es mejor usar el montón para objetos grandes, no para la pila. Esto evita problemas de desbordamiento de pila.

EDITAR: Si está utilizando una aplicación de 64 bits y su sistema operativo y las bibliotecas en tiempo de ejecución son agradables para usted (consulte la publicación de mrree), entonces debería estar bien asignar grandes objetos temporales en la pila. Si su aplicación es de 32 bits y / o su biblioteca OS / runtime no es buena, probablemente necesite asignar estos objetos en el montón.


La pila es una pieza de memoria. El puntero de la pila apunta a la parte superior. Los valores se pueden empujar en la pila y hacer estallar para recuperarlos.

Por ejemplo, si tenemos una función que se llama con dos parámetros (1 byte de tamaño y el otro de 2 bytes, simplemente supongamos que tenemos una PC de 8 bits).

Ambos se presionan en la pila, esto mueve el puntero de la pila hacia arriba:

03: par2 byte2 02: par2 byte1 01: par1

Ahora se llama a la función y el retorno se pone en la pila:

05: ret byte2 04: ret byte1 03: par2 byte2 02: par2 byte1 01: par1

OK, dentro de la función tenemos 2 variables locales; uno de 2 bytes y uno de 4. Para estos, una posición está reservada en la pila, pero primero guardamos el puntero de pila para que sepamos dónde comienzan las conteas contando y los parámetros se encuentran contando hacia abajo.

11: var2 byte4 10: var2 byte3 09: var2 byte2 08: var2 byte1 07: var1 byte2 06: var1 byte1 --------- 05: ret byte2 04: ret byte1 03: par2 byte2 02: par2 byte1 01: par1

Como puedes ver, puedes poner cualquier cosa en la pila mientras tengas espacio. Y de lo contrario obtendrás los fenómenos que le dan nombre a este sitio.


Por "pila es el tamaño de un entero", quiere decir "el puntero de pila es del tamaño de un entero". Apunta a la parte superior de la pila, que es un área enorme de memoria. Bueno, más grande que un número entero.


¡Stack y Heap no son tan diferentes como crees!

Es cierto que algunos sistemas operativos tienen limitaciones de pila. (¡Algunos de ellos también tienen limitaciones desagradables de montón!)

Pero esto ya no es 1985.

¡En estos días, ejecuto Linux!

Mi stacksize predeterminado está limitado a 10 MB. Mi heapsize predeterminado es ilimitado. Es bastante trivial limitar ese tamaño de pila. (* tos * [tcsh] unlimit stacksize * tos *. O setrlimit () )

Las mayores diferencias entre stack y heap son:

  1. las asignaciones de pila simplemente desplazan un puntero (y posiblemente asignan nuevas páginas de memoria si la pila ha crecido lo suficiente). Heap tiene que buscar a través de sus estructuras de datos para encontrar un bloque de memoria adecuado. (Y posiblemente asigne nuevas páginas de memoria también).
  2. la pila sale del alcance cuando finaliza el bloque actual. El montón sale del alcance cuando se llama a delete / free.
  3. Heap se puede fragmentar. Stack nunca se fragmenta.

En Linux, tanto la pila como el montón se manejan a través de la memoria virtual.

En términos de tiempo de asignación, incluso la búsqueda en el montón a través de la memoria mal fragmentada no puede contener una vela al mapeo en las páginas nuevas de la memoria. En lo que respecta al tiempo, las diferencias son insignificantes.

Dependiendo de su sistema operativo, a menudo es solo cuando realmente usa esas nuevas páginas de memoria que están mapeadas (¡ NO durante la asignación de malloc () !) (Es una evaluación lenta).

( new invocaría al constructor, que presumiblemente usaría esas páginas de memoria ...)

Puede manipular el sistema VM creando y destruyendo objetos grandes en la pila o en el montón . Depende de su OS / compilador si la memoria puede / es recuperada por el sistema. Si no se recupera, heap podría reutilizarlo. (Suponiendo que no haya sido reutilizado por otro malloc () mientras tanto.) De forma similar, si la pila no se recupera, simplemente se reutilizaría.

Aunque las páginas que se cambian deberían cambiarse, y ese será tu mayor golpe de tiempo.

¡De todas estas cosas, me preocupo más por la fragmentación de la memoria !

La vida útil (cuando sale del alcance) es siempre el factor decisivo.

Sin embargo, cuando ejecuta programas por largos periodos de tiempo, la fragmentación crea un espacio de memoria que aumenta gradualmente. ¡El cambio constante me mata!

MODIFICADO PARA AGREGAR:

Hombre, he sido mimado!

Algo no estaba sumando aquí ... Supuse que * I * estaba fuera de sí. O todos los demás lo eran. O, más probablemente, ambos. O, solo tal vez, tampoco.

Cualquiera que sea la respuesta, ¡tenía que saber qué estaba pasando!

... Esto va a ser largo. Tengan paciencia conmigo...

He pasado la mayor parte de los últimos 12 años trabajando bajo Linux. Y unos 10 años antes de eso bajo varios sabores de Unix. Mi punto de vista sobre las computadoras es algo parcial. ¡He sido mimado!

He hecho un poco con Windows, pero no lo suficiente como para hablar con autoridad. Tampoco, trágicamente, con Mac OS / Darwin tampoco ... Aunque Mac OS / Darwin / BSD está lo suficientemente cerca como para que parte de mi conocimiento continúe.

Con los punteros de 32 bits, se queda sin espacio de direcciones a 4 GB (2 ^ 32).

Hablando en términos prácticos, STACK + HEAP combinado generalmente está limitado a entre 2 y 4 GB, ya que otras cosas necesitan ser mapeadas allí.

(Hay memoria compartida, bibliotecas compartidas, archivos mapeados en memoria, la imagen ejecutable que ejecuta siempre es agradable, etc.)

En Linux / Unix / MacOS / Darwin / BSD, puede restringir artificialmente HEAP o STACK a los valores arbitrarios que desee en tiempo de ejecución. Pero finalmente hay un límite de sistema duro.

Esta es la distinción (en tcsh) de "límite" frente a "límite -h" . O (en bash) de "ulimit -Sa" vs "ulimit -Ha" . O, programáticamente, de rlim_cur vs rlim_max en struct rlimit .

Ahora llegamos a la parte divertida. Con respecto al Código de Martin York . (¡Gracias Martin ! Buen ejemplo. Siempre es bueno probar cosas!)

Martin presumiblemente corriendo en una Mac. (Una bastante reciente. ¡Su compilación es más nueva que la mía!)

Claro, su código no se ejecutará en su Mac de manera predeterminada. Pero funcionará bien si primero invoca "unlimit stacksize" (tcsh) o "ulimit -Ss unlimited" (bash).

LO IMPORTANTE DEL ASUNTO:

Probando en un antiguo (obsoleto) Linux RH9 2.4.x kernel box, asignando grandes cantidades de STACK O HEAP , o bien uno por sí mismo supera entre 2 y 3 GB. (Tristemente, la RAM + SWAP de la máquina supera un poco menos de 3.5 GB. Es un sistema operativo de 32 bits. Y este no es el único proceso en ejecución. Nos conformamos con lo que tenemos ...)

Entonces, realmente no hay limitaciones en el tamaño de APILAMIENTO frente al tamaño de HEAP en Linux, aparte de los artificiales ...

PERO:

En una Mac, hay un límite de stack duro de 65532 kilobytes . Tiene que ver con cómo se colocan las cosas en la memoria.

Normalmente, piensas en un sistema idealizado como tener STACK en un extremo del espacio de direcciones de memoria, HEAP en el otro, y se construyen uno hacia el otro. Cuando se encuentran, te has quedado sin memoria.

Los Mac parecen pegar sus bibliotecas del sistema compartido en el medio en un desplazamiento fijo que limita ambos lados. Todavía puedes ejecutar el Código de Martin York con "unlimit stacksize", ya que solo está asignando algo así como 8 MiB (<64 MiB) de datos. Pero se quedará sin STACK mucho antes de que se quede sin HEAP .

Estoy en Linux. No lo haré. Lo siento chico Aquí hay un Nickel. Vaya a obtener un mejor sistema operativo.

Hay soluciones para la Mac. Pero se vuelven feos y desordenados e implican retocar los parámetros kernel o linker.

A largo plazo, a menos que Apple haga algo realmente estúpido, los espacios de direcciones de 64 bits harán que todo este asunto de la limitación de la pila sea obsoleto en algún momento Real Soon Now.

Pasando a la Fragmentación:

Cada vez que empuja algo sobre la PILA, se agrega al final. Y se elimina (se retrotrae) cada vez que sale el bloque actual.

Como resultado, no hay agujeros en la PILA . Es todo un gran bloque sólido de memoria usada. Con quizás solo un poco de espacio sin usar al final, todo listo para ser reutilizado.

Por el contrario, cuando HEAP se asigna y se libera, terminas con agujeros de memoria no utilizados. Estos pueden conducir gradualmente a una mayor huella de memoria con el tiempo. No es lo que generalmente queremos decir con una fuga de núcleo, pero los resultados son similares.

La fragmentación de memoria NO es una razón para evitar el almacenamiento de HEAP . Es solo algo a tener en cuenta cuando estás codificando.

Lo que trae a colación SWAP THRASHING :

  • Si ya tiene una gran cantidad de pila asignada / en uso.
  • Si tienes muchos agujeros fragmentados diseminados.
  • Y si tiene una gran cantidad de pequeñas asignaciones.

Luego puede terminar con una gran cantidad de variables, todas utilizadas en una pequeña región localizada del código, dispersas en una gran cantidad de páginas de memoria virtual. (Como en usted está usando 4 bytes en esta página de 2k, y 8 bytes en esa página de 2k, y así sucesivamente para un montón de páginas ...)

Todo lo cual significa que su programa necesita tener un gran número de páginas intercambiadas para ejecutarse. O va a intercambiar páginas constantemente. (A eso lo llamamos golpiza).

Por otro lado, si estas asignaciones pequeñas se hubieran realizado en la PILA , todas estarían ubicadas en un tramo contiguo de memoria. Sería necesario cargar menos páginas de memoria VM. (4 + 8 + ... <2k por la victoria)

Sidenote: Mi razón para llamar la atención sobre esto proviene de un cierto ingeniero eléctrico que sabía que insistía en que todas las matrices se asignaran en el HEAP. Estábamos haciendo matemáticas matriciales para gráficos. A * LOT * de 3 o 4 matrices de elementos. Administrar nuevo / eliminar solo fue una pesadilla. ¡Incluso abstraído en clases causó dolor!

Siguiente tema. Enhebrado:

Sí, los hilos están limitados a pilas muy pequeñas por defecto.

Puede cambiar eso con pthread_attr_setstacksize (). Aunque dependiendo de su implementación de subprocesos, si varios subprocesos comparten el mismo espacio de direcciones de 32 bits, ¡las grandes cantidades individuales de subprocesos serán un problema! ¡Simplemente no hay mucho espacio! Una vez más, la transición a espacios de direcciones (SO) de 64 bits ayudará.

pthread_t threadData; pthread_attr_t threadAttributes; pthread_attr_init( & threadAttributes ); ASSERT_IS( 0, pthread_attr_setdetachstate( & threadAttributes, PTHREAD_CREATE_DETACHED ) ); ASSERT_IS( 0, pthread_attr_setstacksize ( & threadAttributes, 128 * 1024 * 1024 ) ); ASSERT_IS( 0, pthread_create ( & threadData, & threadAttributes, & runthread, NULL ) );

Con respecto a los marcos Stack de Martin York :

Tal vez tú y yo estamos pensando en cosas diferentes?

Cuando pienso en un marco de pila , pienso en una pila de llamadas. Cada función o método tiene su propio marco de pila que consiste en la dirección de retorno, los argumentos y los datos locales.

Nunca he visto ninguna limitación en el tamaño de un marco de pila . Hay limitaciones en el STACK como un todo, pero eso es todo los marcos de pila combinados.

Hay un buen diagrama y discusión de montones de marcos en Wiki.

En una nota final:

En Linux / Unix / MacOS / Darwin / BSD, es posible cambiar las limitaciones de tamaño máximo de APILAMIENTO mediante programación, así como también el límite (tcsh) o ulimit (bash):

struct rlimit limits; limits.rlim_cur = RLIM_INFINITY; limits.rlim_max = RLIM_INFINITY; ASSERT_IS( 0, setrlimit( RLIMIT_STACK, & limits ) );

Simplemente no intente configurarlo en INFINITY en una Mac ... Y cámbielo antes de intentar usarlo. ;-)

Otras lecturas: