Preocupaciones específicas del lenguaje C ++
En primer lugar, no hay una asignación denominada "pila" o "montón" ordenada por C ++ . Si está hablando de objetos automáticos en ámbitos de bloque, incluso no están "asignados". (Por cierto, la duración del almacenamiento automático en C definitivamente NO es lo mismo que "asignado"; este último es "dinámico" en el lenguaje C ++.) La memoria asignada dinámicamente está en la tienda libre , no necesariamente en "el montón", aunque el este último es a menudo la implementación (predeterminada) .
Aunque según las reglas semánticas de la máquina abstracta , los objetos automáticos todavía ocupan memoria, una implementación conforme de C ++ puede ignorar este hecho cuando puede probar que esto no importa (cuando no cambia el comportamiento observable del programa). Este permiso lo otorga la regla as-if en ISO C ++, que también es la cláusula general que permite las optimizaciones habituales (y también hay una regla casi igual en ISO C). Además de la regla as-if, ISO C ++ también tiene reglas de copia de elisiónpara permitir la omisión de creaciones específicas de objetos. Las llamadas al constructor y al destructor involucradas se omiten de este modo. Como resultado, los objetos automáticos (si los hay) en estos constructores y destructores también se eliminan, en comparación con la semántica abstracta ingenua que implica el código fuente.
Por otro lado, la asignación gratuita de la tienda es definitivamente "asignación" por diseño. Según las normas ISO C ++, dicha asignación se puede lograr mediante una llamada de una función de asignación . Sin embargo, desde ISO C ++ 14, hay una nueva regla (que no es como si) para permitir la fusión de ::operator new
llamadas de función de asignación global (es decir ) en casos específicos. Por lo tanto, partes de las operaciones de asignación dinámica también pueden no funcionar como en el caso de los objetos automáticos.
Las funciones de asignación asignan recursos de memoria. Los objetos pueden asignarse aún más en función de la asignación utilizando asignadores. Para los objetos automáticos, se presentan directamente, aunque se puede acceder a la memoria subyacente y usarla para proporcionar memoria a otros objetos (por ubicación new
), pero esto no tiene mucho sentido como la tienda gratuita, porque no hay forma de mover el recursos en otros lugares.
Todas las demás preocupaciones están fuera del alcance de C ++. Sin embargo, pueden seguir siendo significativos.
Acerca de las implementaciones de C ++
C ++ no expone los registros de activación reificados o algunos tipos de continuaciones de primera clase (por ejemplo, por los famosos call/cc
), no hay forma de manipular directamente los marcos de registros de activación, donde la implementación necesita colocar los objetos automáticos. Una vez que no hay interoperaciones (no portátiles) con la implementación subyacente (código "nativo" no portátil, como el código de ensamblaje en línea), una omisión de la asignación subyacente de los marcos puede ser bastante trivial. Por ejemplo, cuando la función llamada está en línea, los marcos pueden fusionarse efectivamente en otros, por lo que no hay forma de mostrar cuál es la "asignación".
Sin embargo, una vez que se respetan los interops, las cosas se vuelven complejas. Una implementación típica de C ++ expondrá la capacidad de interoperabilidad en ISA (arquitectura de conjunto de instrucciones) con algunas convenciones de llamada como el límite binario compartido con el código nativo (máquina de nivel ISA). Esto sería explícitamente costoso, especialmente cuando se mantiene el puntero de la pila , que a menudo se mantiene directamente por un registro de nivel ISA (con probablemente instrucciones específicas de máquina para acceder). El puntero de la pila indica el límite del marco superior de la llamada a la función (actualmente activa). Cuando se ingresa una llamada a una función, se necesita un nuevo marco y el puntero de la pila se agrega o resta (según la convención de ISA) por un valor no menor que el tamaño de marco requerido. Entonces se dice el marco asignadocuando el puntero de la pila después de las operaciones. Los parámetros de las funciones también se pueden pasar al marco de la pila, dependiendo de la convención de llamada utilizada para la llamada. El marco puede contener la memoria de objetos automáticos (probablemente incluyendo los parámetros) especificados por el código fuente de C ++. En el sentido de tales implementaciones, estos objetos están "asignados". Cuando el control sale de la llamada a la función, el marco ya no es necesario, por lo general se libera restaurando el puntero de la pila al estado anterior a la llamada (guardado previamente de acuerdo con la convención de llamada). Esto se puede ver como "desasignación". Estas operaciones hacen que el registro de activación sea efectivamente una estructura de datos LIFO, por lo que a menudo se llama " la pila (de llamadas) ".
Debido a que la mayoría de las implementaciones de C ++ (particularmente las que se dirigen al código nativo de nivel ISA y usan el lenguaje ensamblador como salida inmediata) usan estrategias similares como esta, un esquema de "asignación" tan confuso es popular. Dichas asignaciones (así como las desasignaciones) gastan ciclos de máquina, y puede ser costoso cuando las llamadas (no optimizadas) ocurren con frecuencia, a pesar de que las microarquitecturas de CPU modernas pueden tener optimizaciones complejas implementadas por hardware para el patrón de código común (como usar un motor de pila en la implementación PUSH
/ POP
instrucciones).
Pero de todos modos, en general, es cierto que el costo de la asignación del marco de la pila es significativamente menor que una llamada a una función de asignación que opera el almacén gratuito (a menos que esté totalmente optimizado) , que en sí mismo puede tener cientos de (si no millones de :-) operaciones para mantener el puntero de la pila y otros estados. Las funciones de asignación generalmente se basan en la API proporcionada por el entorno alojado (por ejemplo, tiempo de ejecución proporcionado por el sistema operativo). Diferente al propósito de mantener objetos automáticos para llamadas a funciones, tales asignaciones son de propósito general, por lo que no tendrán una estructura de marco como una pila. Tradicionalmente, asignan espacio desde el almacenamiento de la agrupación llamado montón (o varios montones). A diferencia de la "pila", el concepto "montón" aquí no indica la estructura de datos que se está utilizando;Se deriva de las primeras implementaciones de lenguaje hace décadas. (Por cierto, la pila de llamadas generalmente se asigna con un tamaño fijo o especificado por el usuario desde el montón por el entorno en el inicio del programa o subproceso). La naturaleza de los casos de uso hace que las asignaciones y las desasignaciones de un montón sean mucho más complicadas (que el empuje o el estallido de stack frames), y difícilmente se puede optimizar directamente por hardware.
Efectos sobre el acceso a la memoria
La asignación de pila habitual siempre coloca el nuevo marco en la parte superior, por lo que tiene una localidad bastante buena. Esto es amigable para el caché. OTOH, la memoria asignada aleatoriamente en la tienda gratuita no tiene esa propiedad. Desde ISO C ++ 17, hay plantillas de recursos de grupo proporcionadas por <memory>
. El propósito directo de dicha interfaz es permitir que los resultados de asignaciones consecutivas estén muy juntos en la memoria. Esto reconoce el hecho de que esta estrategia es generalmente buena para el rendimiento con implementaciones contemporáneas, por ejemplo, amigable para el caché en arquitecturas modernas. Sin embargo, se trata del rendimiento del acceso en lugar de la asignación .
Concurrencia
La expectativa de acceso concurrente a la memoria puede tener diferentes efectos entre la pila y los montones. Una pila de llamadas generalmente es propiedad exclusiva de un subproceso de ejecución en una implementación de C ++. OTOH, los montones a menudo se comparten entre los hilos en un proceso. Para tales montones, las funciones de asignación y desasignación deben proteger la estructura de datos administrativos internos compartidos de la carrera de datos. Como resultado, las asignaciones de montón y las desasignaciones pueden tener una sobrecarga adicional debido a las operaciones de sincronización interna.
Eficiencia Espacial
Debido a la naturaleza de los casos de uso y las estructuras de datos internas, los montones pueden sufrir fragmentación de la memoria interna , mientras que la pila no. Esto no tiene un impacto directo en el rendimiento de la asignación de memoria, pero en un sistema con memoria virtual , la baja eficiencia de espacio puede degenerar el rendimiento general del acceso a la memoria. Esto es particularmente horrible cuando HDD se usa como un intercambio de memoria física. Puede causar una latencia bastante larga, a veces miles de millones de ciclos.
Limitaciones de las asignaciones de pila
Aunque las asignaciones de pila son a menudo superiores en rendimiento que las asignaciones de pila en realidad, ciertamente no significa que las asignaciones de pila siempre puedan reemplazar las asignaciones de pila.
Primero, no hay forma de asignar espacio en la pila con un tamaño especificado en tiempo de ejecución de forma portátil con ISO C ++. Hay extensiones proporcionadas por implementaciones como alloca
VLA (matriz de longitud variable) de G ++, pero hay razones para evitarlas. (IIRC, la fuente de Linux elimina el uso de VLA recientemente). (También tenga en cuenta que ISO C99 tiene VLA obligatorio, pero ISO C11 hace que el soporte sea opcional).
En segundo lugar, no existe una forma confiable y portátil de detectar el agotamiento del espacio de la pila. Esto a menudo se llama desbordamiento de pila (hmm, la etimología de este sitio) , pero probablemente más exactamente, desbordamiento de pila . En realidad, esto a menudo provoca un acceso no válido a la memoria, y el estado del programa se corrompe (... o, lo que es peor, un agujero de seguridad). De hecho, ISO C ++ no tiene el concepto de "la pila" y lo convierte en un comportamiento indefinido cuando el recurso se agota . Tenga cuidado con la cantidad de espacio que debe dejarse para los objetos automáticos.
Si se agota el espacio de la pila, hay demasiados objetos asignados en la pila, que pueden ser causados por demasiadas llamadas activas de funciones o el uso incorrecto de objetos automáticos. Tales casos pueden sugerir la existencia de errores, por ejemplo, una llamada de función recursiva sin condiciones de salida correctas.
Sin embargo, a veces se desean llamadas recursivas profundas. En implementaciones de lenguajes que requieren soporte de llamadas activas no vinculadas (donde la profundidad de la llamada solo está limitada por la memoria total), es imposible usar la pila de llamadas nativas (contemporáneas) directamente como el registro de activación del lenguaje objetivo como las implementaciones típicas de C ++. Para solucionar el problema, se necesitan formas alternativas de construcción de registros de activación. Por ejemplo, SML / NJ asigna explícitamente marcos en el montón y utiliza pilas de cactus . La complicada asignación de tales marcos de registro de activación generalmente no es tan rápida como los marcos de la pila de llamadas. Sin embargo, si dichos idiomas se implementan aún más con la garantía de adecuada recursión de cola, la asignación directa de la pila en el lenguaje de objetos (es decir, el "objeto" en el lenguaje no se almacena como referencias, pero los valores primitivos nativos que pueden asignarse uno a uno a objetos C ++ no compartidos) es aún más complicado con más pena de desempeño en general. Cuando se usa C ++ para implementar dichos lenguajes, es difícil estimar los impactos en el rendimiento.