¿Podría ser más eficiente que los sistemas en general eliminen las pilas y solo usen Heap para la administración de memoria?

14

Me parece que todo lo que se puede hacer con una pila se puede hacer con el montón, pero no todo lo que se puede hacer con el montón se puede hacer con la pila. ¿Es eso correcto? Entonces, por simplicidad, e incluso si perdemos una pequeña cantidad de rendimiento con ciertas cargas de trabajo, ¿no podría ser mejor ir con un solo estándar (es decir, el montón)?

Piense en la compensación entre modularidad y rendimiento. Sé que esa no es la mejor manera de describir este escenario, pero en general parece que la simplicidad de comprensión y diseño podría ser una mejor opción, incluso si existe la posibilidad de un mejor rendimiento.

Templario oscuro
fuente
1
En C y C ++, debe desasignar explícitamente la memoria asignada en el montón. Eso no es más simple.
user16764
Utilicé una implementación de C #, los perfiles revelaron que los objetos de la pila se asignaron en un área similar a un montón con una terrible recolección de basura. ¿Mi solución? Mueva todo lo posible (por ejemplo, variables de bucle, variables temporales, etc.) a la memoria de montón persistente. Hizo que el programa comiera 10 veces la RAM y corriera a 10 veces la velocidad.
imallett
@ IanMallett: No entiendo su explicación del problema y la solución. ¿Tienes un enlace con más información en alguna parte? Por lo general, encuentro que la asignación basada en pila es más rápida.
Frank Hileman
@FrankHileman el problema básico era este: la implementación de C # que estaba usando tenía una velocidad de recolección de basura extremadamente pobre. La "solución" era hacer que todas las variables fueran persistentes para que en el tiempo de ejecución no ocurrieran operaciones de memoria. Hace un tiempo escribí un artículo de opinión sobre el desarrollo de C # / XNA en general que también analiza parte del contexto.
imallett
@IanMallett: gracias. Como ex desarrollador de C / C ++ que usa principalmente C # en estos días, mi experiencia ha sido bastante diferente. Encuentro que las bibliotecas son el mayor problema. Parece que la plataforma XBox360 estaba a medias para los desarrolladores de .net. Por lo general, cuando tengo problemas de GC, cambio a la agrupación. Sí ayuda.
Frank Hileman

Respuestas:

30

Los montones son malos en la asignación rápida de memoria y la desasignación. Si desea obtener muchas pequeñas cantidades de memoria durante un tiempo limitado, un montón no es su mejor opción. Una pila, con su algoritmo de asignación / desasignación súper simple, sobresale naturalmente en esto (aún más si está integrado en el hardware), por lo que la gente lo usa para cosas como pasar argumentos a funciones y almacenar variables locales, la mayoría La desventaja importante es que tiene un espacio limitado, por lo que mantener objetos grandes en él o tratar de usarlo para objetos de larga duración son malas ideas.

Deshacerse de la pila por completo en aras de simplificar un lenguaje de programación es la manera incorrecta de la OMI: un mejor enfoque sería abstraer las diferencias, dejar que el compilador descubra qué tipo de almacenamiento usar, mientras el programador reúne más alto- construcciones de nivel que están más cerca de la forma en que los humanos piensan, y de hecho, los lenguajes de alto nivel como C #, Java, Python, etc. hacen exactamente esto. Ofrecen una sintaxis casi idéntica para los objetos asignados al montón y las primitivas asignadas a la pila ('tipos de referencia' frente a 'tipos de valor' en la jerga de .NET), ya sea completamente transparente o con algunas diferencias funcionales que debe comprender para usar el lenguaje correctamente (pero en realidad no tiene que saber cómo funcionan internamente una pila y un montón).

tdammers
fuente
2
WOW ESTO FUE BUENO :) ¡Realmente conciso e informativo para un principiante!
Dark Templar
1
En muchas CPU, la pila se maneja en hardware, lo cual es un problema fuera del lenguaje pero juega un papel importante en el tiempo de ejecución.
Patrick Hughes
@Patrick Hughes: Sí, pero el montón también se encuentra en el hardware, ¿no?
Dark Templar
@Dark Lo que Patrick probablemente quiera decir es que las arquitecturas como x86 tienen registros especiales para administrar la pila e instrucciones especiales para colocar o quitar algo en la pila. Eso lo hace bastante rápido.
FUZxxl
3
@Donal Fellows: Todo es verdad. Pero el punto es que las pilas y los montones tienen sus puntos fuertes y débiles, y usarlos en consecuencia producirá el código más eficiente.
tdammers
8

En pocas palabras, una pila no es un poco de rendimiento. Es cientos o miles de veces más rápido que el montón. Además, la mayoría de las máquinas modernas tienen soporte de hardware para la pila (como x86) y esa funcionalidad de hardware para, por ejemplo, la pila de llamadas no se puede eliminar.

DeadMG
fuente
¿Qué quiere decir cuando dice que las máquinas modernas tienen soporte de hardware para la pila? La pila en sí ya está EN el hardware, ¿no?
Dark Templar
1
x86 tiene registros e instrucciones especiales para manejar la pila. x86 no tiene soporte para montones, tales cosas son creadas por el sistema operativo.
Pubby
8

No

El área de pila en C ++ es increíblemente rápida en comparación. Me atrevo a decir que ningún desarrollador experimentado de C ++ estaría abierto a deshabilitar esa funcionalidad.

Con C ++, tiene elección y control. Los diseñadores no estaban particularmente inclinados a introducir características que agregaran tiempo o espacio de ejecución significativo.

Ejerciendo esa elección

Si desea crear una biblioteca o programa que requiera que cada objeto se asigne dinámicamente, puede hacerlo con C ++. Se ejecutaría relativamente lento, pero entonces podría tener esa 'modularidad'. Para el resto de nosotros, la modularidad siempre es opcional, instálela según sea necesario porque ambas son necesarias para implementaciones buenas / rápidas.

Alternativas

Hay otros idiomas que requieren que el almacenamiento para cada objeto se cree en el montón; es bastante lento, de modo que compromete los diseños (programas del mundo real) de una manera peor que tener que aprender ambos (IMO).

Ambos son importantes, y C ++ le brinda el poder de usar ambos de manera efectiva para cada escenario dado. Dicho esto, el lenguaje C ++ puede no ser ideal para su diseño, si estos factores en su OP son importantes para usted (por ejemplo, lea en lenguajes de nivel superior).

justin
fuente
El montón es en realidad la misma velocidad que la pila, pero no tiene el soporte de hardware especializado para la asignación. Por otro lado, hay formas de acelerar mucho los montones (sujeto a una serie de condiciones que los convierten en técnicas exclusivas para expertos).
Donal Fellows
@DonalFellows: el soporte de hardware para las pilas es irrelevante. Lo importante es saber que cada vez que se libera algo, se puede liberar cualquier cosa asignada después. Algunos lenguajes de programación no tienen montones que pueden liberar objetos de forma independiente, sino que solo tienen un método de "liberar todo asignado después".
supercat
6

Entonces, por simplicidad, e incluso si perdemos una pequeña cantidad de rendimiento con ciertas cargas de trabajo, ¿no podría ser mejor ir con un solo estándar (es decir, el montón)?

En realidad, ¡el rendimiento es probable que sea considerable!

Como otros han señalado, las pilas son una estructura extremadamente eficiente para administrar datos que obedecen las reglas LIFO (último en entrar, primero en salir). La asignación / liberación de memoria en la pila suele ser solo un cambio en un registro en la CPU. Cambiar un registro es casi siempre una de las operaciones más rápidas que puede realizar un procesador.

El montón suele ser una estructura de datos bastante compleja y la asignación / liberación de memoria requerirá muchas instrucciones para hacer toda la contabilidad asociada. Peor aún, en implementaciones comunes, cada llamada para trabajar con el montón tiene el potencial de resultar en una llamada al sistema operativo. ¡Las llamadas al sistema operativo requieren mucho tiempo! El programa generalmente tiene que cambiar del modo de usuario al modo kernel, y cada vez que esto sucede, el sistema operativo puede decidir que otros programas tienen necesidades más urgentes y que su programa tendrá que esperar.

Charles E. Grant
fuente
5

Simula usó el montón para todo. Poner todo en el montón siempre induce un nivel más de indirección para las variables locales, y ejerce una presión adicional sobre el recolector de basura (debe tener en cuenta que los recolectores de basura realmente apestaron en ese entonces). Eso es en parte por qué Bjarne inventó C ++.

flujo libre
fuente
Entonces, ¿básicamente C ++ solo usa el montón también?
Dark Templar
2
@Dark: ¿Qué? No. La falta de stack en Simula fue una inspiración para hacerlo mejor.
fredoverflow
Ah ya veo lo que quieres decir ahora! Gracias +1 :)
Dark Templar
3

Las pilas son extremadamente eficientes para los datos LIFO, como los metadatos asociados con las llamadas a funciones, por ejemplo. La pila también aprovecha las características de diseño inherentes de la CPU. Dado que el rendimiento en este nivel es fundamental para casi todo lo demás en un proceso, tomar ese "pequeño" golpe a ese nivel se propagará ampliamente. Además, el sistema operativo puede mover la memoria del montón, lo que sería mortal para las pilas. Si bien se puede implementar una pila en el montón, requiere una sobrecarga que afectará literalmente cada parte del proceso al nivel más granular.

kylben
fuente
2

"eficiente" en términos de escribir código tal vez, pero ciertamente no en términos de la eficiencia de su software. Las asignaciones de pila son esencialmente gratuitas (solo se necesitan unas pocas instrucciones de la máquina para mover el puntero de la pila y reservar espacio en la pila para las variables locales).

Dado que la asignación de la pila casi no lleva tiempo, una asignación incluso en un montón muy eficiente será 100k (si no 1M +) de veces más lenta.

Ahora imagine cuántas variables locales y otras estructuras de datos utiliza una aplicación típica. Cada pequeña "i" que usas como contador de bucles se asigna un millón de veces más lento.

Claro, si el hardware es lo suficientemente rápido, puede escribir una aplicación que solo use el montón. Pero ahora imagina qué tipo de aplicación podrías escribir si aprovechas el montón y utilizas el mismo hardware.

DXM
fuente
Cuando dice "imagine cuántas variables locales y otras estructuras de datos utiliza una aplicación típica", ¿a qué otras estructuras de datos se refiere específicamente?
Dark Templar
1
¿Los valores "100k" y "1M +" son de alguna manera científicos? ¿O es solo una forma de decir "mucho"?
Bruno Reis
@Bruno - En mi humilde opinión, los números de 100K y 1M que utilicé son en realidad una estimación conservadora para probar un punto. Si está familiarizado con VS y C ++, escriba un programa que asigne 100 bytes en la pila y escriba uno que asigne 100 bytes en el montón. Luego cambie a la vista de desmontaje y simplemente cuente el número de instrucciones de ensamblaje que toma cada asignación. Las operaciones de almacenamiento dinámico suelen ser varias llamadas de función a la DLL de Windows, hay cubos y listas vinculadas, y luego hay fusión y otros algoritmos. Con la pila, puede reducirse a una instrucción de ensamblaje "agregar esp, 100" ...
DXM
2
¿"100k (si no 1M +) de veces más lento"? Eso es un poco exagerado. Que sea dos órdenes de magnitud más lento, quizás tres, pero eso es todo. Al menos, mi Linux es capaz de hacer asignaciones de almacenamiento dinámico de 100M (+ algunas instrucciones que lo rodean) en menos de 6 segundos en un Core i5, que no pueden ser más de unos cientos de instrucciones por asignación; en realidad, es casi seguro que es menor. Si es seis órdenes de magnitud más lento que la pila, hay algo muy mal con la implementación del montón del sistema operativo. Claro que hay mucho mal con Windows, pero eso ...
Leftaroundabout
1
los moderadores probablemente estén a punto de matar todo este hilo de comentarios. Así que aquí está el trato, reconozco que los números reales fueron extraídos de mi ..., pero aceptemos que el factor es realmente muy grande y no hacemos más comentarios :)
DXM
2

Es posible que le interese "La recolección de basura es rápida, pero una pila es más rápida".

http://dspace.mit.edu/bitstream/handle/1721.1/6622/AIM-1462.ps.Z

Si lo leí correctamente, estos chicos modificaron un compilador de C para asignar "cuadros de pila" en el montón, y luego usar la recolección de basura para desasignar los cuadros en lugar de hacer estallar la pila.

Los "marcos de pila" asignados a la pila superan decisivamente a los "marcos de pila" asignados al montón.

Bruce Ediger
fuente
1

¿Cómo va a funcionar la pila de llamadas en un montón? Esencialmente, tendría que asignar una pila en el montón en cada programa, entonces, ¿por qué no hacer que el hardware OS + lo haga por usted?

Si quieres que las cosas sean realmente simples y eficientes, solo dale al usuario su porción de memoria y deja que se ocupe de eso. Por supuesto, nadie quiere implementar todo por sí mismo y es por eso que tenemos una pila y un montón.

Pubby
fuente
Estrictamente hablando, una "pila de llamadas" no es una característica requerida de un entorno de tiempo de ejecución de lenguaje de programación. Por ejemplo, una implementación directa de un lenguaje funcional evaluado perezosamente por reducción de gráficos (que he codificado) no tiene pila de llamadas. Pero la pila de llamadas es una técnica muy útil y ampliamente utilizada, especialmente porque los procesadores modernos suponen que la usa y están optimizados para su uso.
Ben
@Ben: si bien es cierto (y algo bueno) abstraer cosas como la asignación de memoria de un idioma, esto no cambia la arquitectura de computadora que prevalece ahora. Por lo tanto, su código de reducción de gráficos seguirá utilizando la pila cuando se ejecute, nos guste o no.
Ingo
@Ingo No realmente en ningún sentido significativo. Claro, el sistema operativo inicializará una sección de memoria tradicionalmente llamada "la pila", y habrá un registro apuntando a ella. Pero las funciones en el idioma de origen no terminan representadas como marcos de pila en el orden de las llamadas. La ejecución de la función está totalmente representada por la manipulación de estructuras de datos en el montón. Incluso sin usar la optimización de última llamada, no es posible "desbordar la pila". A eso me refiero cuando digo que no hay nada fundamental sobre "la pila de llamadas".
Ben
No hablo de las funciones del lenguaje fuente, sino de las funciones en el intérprete (o lo que sea) que realmente realizan la reducción gráfica. Esos necesitarán una pila. Esto es evidente, ya que el hardware contemporáneo no hace reducción de gráficos. Por lo tanto, su algoritmo de reducción de gráficos finalmente se asigna a la oda de la máquina, y apuesto a que hay llamadas de subrutina entre ellos. QED
Ingo
1

Se requieren tanto la pila como el montón. Se utilizan en diferentes situaciones, por ejemplo:

  1. La asignación del montón tiene una limitación que sizeof (a [0]) == sizeof (a [1])
  2. La asignación de pila tiene una limitación de que sizeof (a) es constante en tiempo de compilación
  3. La asignación de montón puede hacer bucles, gráficos, etc. estructuras de datos complejas
  4. La asignación de pila puede hacer árboles del tamaño de tiempo de compilación
  5. El montón requiere el seguimiento de la propiedad
  6. La asignación de la pila y la desasignación es automática
  7. La memoria de montón se puede pasar fácilmente de un ámbito a otro a través de punteros
  8. La memoria de pila es local para cada función y los objetos deben moverse al alcance superior para extender su vida útil (o almacenarse dentro de los objetos en lugar de dentro de las funciones miembro)
  9. El montón es malo para el rendimiento
  10. La pila es bastante rápida
  11. Los objetos de montón se devuelven de las funciones a través de punteros que toman posesión. O shared_ptrs.
  12. Los objetos de pila se devuelven de las funciones a través de referencias que no toman posesión.
  13. El montón requiere hacer coincidir cada nuevo con el tipo correcto de eliminar o eliminar []
  14. Los objetos de pila usan RAII y listas de inicialización de constructor
  15. Los objetos de montón se pueden inicializar en cualquier punto dentro de una función y no pueden usar parámetros de constructor
  16. Los objetos de pila usan parámetros de constructor para la inicialización
  17. El montón utiliza matrices y el tamaño de la matriz puede cambiar en tiempo de ejecución
  18. La pila es para objetos individuales, y el tamaño se fija en tiempo de compilación

Básicamente, los mecanismos no se pueden comparar en absoluto porque muchos detalles son diferentes. Lo único común con ellos es que ambos manejan la memoria de alguna manera.

tp1
fuente
1

Las computadoras modernas tienen varias capas de memoria caché además de un sistema de memoria principal grande pero lento. Uno puede hacer docenas de accesos a la memoria caché más rápida en el tiempo requerido para leer o escribir un byte desde el sistema de memoria principal. Por lo tanto, acceder a una ubicación mil veces es mucho más rápido que acceder a 1,000 (o incluso 100) ubicaciones independientes una vez cada una. Debido a que la mayoría de las aplicaciones asignan y desasignan repetidamente pequeñas cantidades de memoria cerca de la parte superior de la pila, las ubicaciones en la parte superior de la pila se usan y reutilizan una cantidad enorme, de modo que la gran mayoría (99% + en una aplicación típica) de los accesos a la pila se pueden manejar usando memoria caché.

Por el contrario, si una aplicación creara y abandonara repetidamente objetos de almacenamiento dinámico para almacenar información de continuación, cada versión de cada objeto de pila que se haya creado debería escribirse en la memoria principal. Incluso si la gran mayoría de estos objetos fueran completamente inútiles para cuando la CPU quisiera reciclar las páginas de caché en las que comenzaron, la CPU no tendría forma de saberlo. En consecuencia, la CPU tendría que perder mucho tiempo realizando grabaciones lentas de memoria de información inútil. No es exactamente una receta para la velocidad.

Otra cosa a considerar es que en muchos casos es útil saber que una referencia de objeto pasada a una rutina no se usará una vez que la rutina salga. Si se pasan parámetros y variables locales a través de la pila, y si la inspección del código de la rutina revela que no persiste una copia de la referencia pasada, entonces el código que llama a la rutina puede estar seguro de que si no hay una referencia externa a la el objeto existía antes de la llamada, ninguno existirá después. Por el contrario, si los parámetros se pasaran a través de objetos de montón, los conceptos como "después de que una rutina regrese" se vuelven algo más nebulosos, ya que si el código guardara una copia de la continuación, sería posible que la rutina "regrese" más de una vez después de un Llamada única.

Super gato
fuente