c++ static garbage-collection stack heap

Stack, Static y Heap en C++



garbage-collection (9)

He buscado, pero no he entendido muy bien estos tres conceptos. ¿Cuándo debo usar la asignación dinámica (en el montón) y cuál es su ventaja real? ¿Cuáles son los problemas de estática y pila? ¿Podría escribir una aplicación completa sin asignar variables en el montón?

Escuché que otros idiomas incorporan un "recolector de basura" para que no tenga que preocuparse por la memoria. ¿Qué hace el recolector de basura?

¿Qué podría hacer manipulando la memoria usted mismo que no podría hacer con este recolector de basura?

Una vez que alguien me dijo eso con esta declaración:

int * asafe=new int;

Tengo un "puntero a un puntero". Qué significa eso? Es diferente de:

asafe=new int;

?


¿Cuáles son los problemas de estática y pila?

El problema con la asignación "estática" es que la asignación se realiza en tiempo de compilación: no se puede usar para asignar un número variable de datos, cuyo número no se conoce hasta el tiempo de ejecución.

El problema con la asignación en la "pila" es que la asignación se destruye tan pronto como se devuelve la subrutina que hace la asignación.

¿Podría escribir una aplicación completa sin asignar variables en el montón?

Quizás, pero no una aplicación no trivial, normal, grande (pero los llamados programas "integrados" podrían escribirse sin el montón, usando un subconjunto de C ++).

¿Qué recolector de basura hace?

Sigue mirando sus datos ("marcar y barrer") para detectar cuándo su aplicación ya no hace referencia a ella. Esto es conveniente para la aplicación, porque la aplicación no necesita desasignar los datos ... pero el recolector de basura puede ser costoso desde el punto de vista computacional.

Los recolectores de basura no son una característica habitual de la programación en C ++.

¿Qué podría hacer manipulando la memoria usted mismo que no podría hacer con este recolector de basura?

Aprenda los mecanismos de C ++ para la desasignación de memoria determinista:

  • ''estático'': nunca desasignado
  • ''stack'': tan pronto como la variable "sale del alcance"
  • ''Heap'': cuando se borra el puntero (eliminado explícitamente por la aplicación, o implícitamente eliminado dentro de una subrutina en algún otro lugar)

¿Qué sucede si su programa no sabe por adelantado cuánta memoria asignar (por lo tanto, no puede usar variables de pila)? Diga listas vinculadas, las listas pueden crecer sin saber por adelantado cuál es su tamaño. Por lo tanto, la asignación en un montón tiene sentido para una lista vinculada cuando no se sabe cuántos elementos se insertarán en ella.


Estoy seguro de que a uno de los pedantes se le ocurrirá una respuesta mejor en breve, pero la principal diferencia es la velocidad y el tamaño.

Apilar

Dramáticamente más rápido de asignar. Se realiza en O (1) ya que se asigna al configurar el marco de pila, por lo que es esencialmente libre. El inconveniente es que si te quedas sin espacio en la pila tienes huesos. Puedes ajustar el tamaño de la pila, pero IIRC tienes ~ 2MB para jugar. Además, tan pronto como salga de la función, todo en la pila se borrará. Entonces puede ser problemático referirse a esto más tarde. (Los punteros para apilar objetos asignados generan errores).

Montón

Dramáticamente más lento de asignar. Pero tienes GB para jugar y señalar.

Recolector de basura

El recolector de basura es un código que se ejecuta en segundo plano y libera memoria. Cuando asigna memoria en el montón, es muy fácil olvidarse de liberarla, lo que se conoce como pérdida de memoria. Con el tiempo, la memoria que consume su aplicación crece y crece hasta que se bloquea. Tener un recolector de basura periódicamente para liberar la memoria que ya no necesita ayuda a eliminar esta clase de errores. Por supuesto, esto tiene un precio, ya que el recolector de basura ralentiza las cosas.


La asignación de la memoria de la pila (variables de función, variables locales) puede ser problemático cuando la pila es demasiado "profunda" y se desborda la memoria disponible para apilar las asignaciones. El montón es para objetos a los que se debe acceder desde varios subprocesos o durante todo el ciclo de vida del programa. Puede escribir un programa completo sin usar el montón.

Puede perder memoria fácilmente sin un recolector de basura, pero también puede dictar cuándo se liberan los objetos y la memoria. He tenido problemas con Java cuando ejecuta el GC y tengo un proceso en tiempo real, porque el GC es un hilo exclusivo (no se puede ejecutar nada más). Entonces, si el rendimiento es crítico y puede garantizar que no haya objetos filtrados, no usar GC es muy útil. De lo contrario, solo hace que odies la vida cuando tu aplicación consume memoria y tienes que rastrear el origen de una fuga.


La siguiente es, por supuesto, no muy precisa. Tómalo con un grano de sal cuando lo leas :)

Bueno, las tres cosas a las que te refieres son la duración de almacenamiento automática, estática y dinámica , que tiene que ver con la duración de los objetos y con el comienzo de la vida.

Duración de almacenamiento automático

Utiliza la duración de almacenamiento automático para datos breves y de poca duración , que solo se necesita localmente en algún bloque:

if(some condition) { int a[3]; // array a has automatic storage duration fill_it(a); print_it(a); }

La vida útil termina tan pronto como salimos del bloque, y comienza tan pronto como se define el objeto. Son el tipo más simple de duración de almacenamiento, y son mucho más rápidos que en particular la duración de almacenamiento dinámico.

Duración de almacenamiento estático

Utiliza la duración de almacenamiento estático para variables libres, a las que cualquier código puede tener acceso en todo momento, si su alcance permite tal uso (ámbito de espacio de nombre) y para variables locales que necesitan extender su vida útil a través de la salida de su alcance (ámbito local) y para variables miembro que necesitan ser compartidas por todos los objetos de su clase (alcance de clase). Su duración depende del alcance en el que se encuentren. Pueden tener alcance de espacio de nombres y ámbito local y alcance de clase . Lo que es cierto acerca de ambos es que, una vez que comienza su vida, la duración de la vida finaliza al final del programa . Aquí hay dos ejemplos:

// static storage duration. in global namespace scope string globalA; int main() { foo(); foo(); } void foo() { // static storage duration. in local scope static string localA; localA += "ab" cout << localA; }

El programa imprime ababab , porque localA no se destruye al salir de su bloque. Puede decir que los objetos que tienen alcance local comienzan a funcionar cuando el control llega a su definición . Para localA , sucede cuando se ingresa el cuerpo de la función. Para los objetos en el ámbito del espacio de nombres, la duración comienza al inicio del programa . Lo mismo es cierto para objetos estáticos de alcance de clase:

class A { static string classScopeA; }; string A::classScopeA; A a, b; &a.classScopeA == &b.classScopeA == &A::classScopeA;

Como ve, classScopeA no está vinculado a objetos particulares de su clase, sino a la clase misma. La dirección de los tres nombres anteriores es la misma, y ​​todos denotan el mismo objeto. Hay una regla especial sobre cuándo y cómo se inicializan los objetos estáticos, pero no nos preocupemos por eso ahora. Eso significa el término fiasco de orden de inicialización estática .

Duración de almacenamiento dinámico

La última duración de almacenamiento es dinámica. Lo usa si quiere tener objetos en vivo en otra isla, y quiere poner punteros alrededor de esa referencia. También los usa si sus objetos son grandes y si desea crear matrices de tamaño solo conocidas en tiempo de ejecución . Debido a esta flexibilidad, los objetos que tienen una duración de almacenamiento dinámico son complicados y de administración lenta. Los objetos que tienen esa duración dinámica comienzan a durar cuando ocurre una nueva invocación de operador apropiada:

int main() { // the object that s points to has dynamic storage // duration string *s = new string; // pass a pointer pointing to the object around. // the object itself isn''t touched foo(s); delete s; } void foo(string *s) { cout << s->size(); }

Su tiempo de vida finaliza solo cuando se llama eliminar para ellos. Si olvidas eso, esos objetos nunca terminan de por vida. Y los objetos de clase que definen un constructor declarado por el usuario no tendrán llamados sus destructores. Los objetos que tienen una duración de almacenamiento dinámica requieren el manejo manual de su vida útil y de los recursos de memoria asociados. Las bibliotecas existen para facilitar el uso de ellas. La recolección de basura explícita para objetos particulares se puede establecer usando un puntero inteligente:

int main() { shared_ptr<string> s(new string); foo(s); } void foo(shared_ptr<string> s) { cout << s->size(); }

No tiene que preocuparse por llamar a eliminar: el ptr compartido lo hace por usted, si el último puntero que hace referencia al objeto queda fuera del alcance. El ptr compartido en sí tiene una duración de almacenamiento automática. Por lo tanto, su duración se gestiona automáticamente, lo que le permite comprobar si debe eliminar el objeto apuntado a dinámico en su destructor. Para la referencia shared_ptr, vea impulsar documentos: http://www.boost.org/doc/libs/1_37_0/libs/smart_ptr/shared_ptr.htm


Se ha dicho con detalle, al igual que "la respuesta breve":

  • variable estática (clase)
    duración = tiempo de ejecución del programa (1)
    visibilidad = determinada por modificadores de acceso (privado / protegido / público)

  • variable estática (alcance global)
    duración = tiempo de ejecución del programa (1)
    visibility = la unidad de compilación en la que se crea una instancia (2)

  • variable de montón
    duración = definido por usted (nuevo para eliminar)
    visibilidad = definida por usted (lo que le asigna el puntero)

  • variable de pila
    visibility = from declaration hasta que se sale del alcance
    tiempo de vida = desde la declaración hasta que se abandona el ámbito de declaración

(1) más exactamente: desde la inicialización hasta la desinicialización de la unidad de compilación (es decir, archivo C / C ++). El orden de inicialización de las unidades de compilación no está definido por el estándar.

(2) Cuidado: si instancia una variable estática en un encabezado, cada unidad de compilación obtiene su propia copia.


Stack es una memoria asignada por el compilador, cada vez que compilamos el programa, en el compilador predeterminado asignamos algo de memoria del sistema operativo (podemos cambiar las configuraciones de la configuración del compilador en tu IDE) y OS es el que te da la memoria, depende en muchas memorias disponibles en el sistema y muchas otras cosas, y al llegar a la pila de memoria se asignan cuando declaramos una variable que copian (ref como formales) esas variables son empujadas a la pila. Siguen algunas convenciones de nombres por defecto su CDECL en Visual Studios ex: notación infija: c = a + b; el empuje de la pila se realiza de derecha a izquierda PUSHING, b para apilar, operador, apilar y resultado de esos i, ec para apilar. En la notación prefijada: = + cab Aquí todas las variables se presionan para apilarse primero (de derecha a izquierda) y luego se realiza la operación. Esta memoria asignada por el compilador es fija. Asumamos que se asigna 1MB de memoria a nuestra aplicación, digamos que las variables utilizan 700kb de memoria (todas las variables locales se envían a la pila a menos que se asignen dinámicamente), por lo que la memoria restante de 324kb se asigna a Heap. Y esta pila tiene menos tiempo de vida, cuando el alcance de la función termina estas pilas se borra.


Una ventaja de GC en algunas situaciones es una molestia en otros; confiar en GC alienta a no pensar mucho al respecto. En teoría, espera hasta el período ''inactivo'' o hasta que sea absolutamente necesario, cuando roba ancho de banda y causa latencia de respuesta en su aplicación.

Pero no tienes que ''no pensar en eso''. Al igual que con todo lo demás en las aplicaciones multiproceso, cuando puede ceder, puede ceder. Entonces, por ejemplo, en .Net, es posible solicitar un GC; Al hacer esto, en lugar de un GC de funcionamiento más lento y menos frecuente, puede tener un CG más corto y más frecuente, y extender la latencia asociada con esta sobrecarga.

Pero esto derrota la atracción principal de GC, que parece ser "alentada a no tener que pensar mucho sobre eso porque es automático".

Si primero estuvo expuesto a la programación antes de que GC se volviera frecuente y se sintiera cómodo con malloc / free y new / delete, entonces incluso podría ser que encuentre GC un poco molesto y / o desconfiado (como uno podría desconfiar de '' optimización, ''que ha tenido una historia accidentada). Muchas aplicaciones toleran latencia aleatoria. Pero para las aplicaciones que no lo hacen, donde la latencia aleatoria es menos aceptable, una reacción común es evitar los entornos de GC y avanzar en la dirección de un código puramente no administrado (o dios no lo permita, un lenguaje de ensamblaje que muere durante mucho tiempo).

Hace un tiempo tuve un estudiante de verano, un interno, un chico inteligente, que fue destetado en GC; era tan firme acerca de la superiortia de GC que incluso cuando programaba en C / C ++ no administrado se negaba a seguir el modelo malloc / free new / delete porque, cito, "no deberías tener que hacer esto en un lenguaje de programación moderno". ¿Y sabes? Para aplicaciones pequeñas y de ejecución corta, de hecho puede salirse con la suya, pero no por mucho tiempo ejecutar aplicaciones de rendimiento.


Se hizo una pregunta similar , pero no preguntó sobre la estática.

Resumen de lo que son la memoria estática, el montón y la pila son:

  • Una variable estática es básicamente una variable global, incluso si no puede acceder a ella globalmente. Por lo general, hay una dirección que se encuentra en el ejecutable. Solo hay una copia para todo el programa. No importa cuántas veces vaya a una llamada de función (o clase) (y en cuántos subprocesos!) La variable hace referencia a la misma ubicación de memoria.

  • El montón es un montón de memoria que se puede usar dinámicamente. Si desea 4kb para un objeto, el asignador dinámico buscará en su lista de espacio libre en el montón, seleccionará un fragmento de 4kb y se lo dará a usted. En general, el asignador de memoria dinámica (malloc, nuevo, etc.) comienza al final de la memoria y funciona hacia atrás.

  • Explicar cómo una pila crece y se contrae está un poco fuera del alcance de esta respuesta, pero basta con decir que siempre se agrega y se elimina solo desde el final. Las pilas generalmente comienzan altas y crecen hacia direcciones más bajas. Se queda sin memoria cuando la pila se encuentra con el asignador dinámico en algún punto intermedio (pero se refiere a la fragmentación y la memoria física frente a la virtual). Múltiples hilos requerirán múltiples pilas (el proceso generalmente reserva un tamaño mínimo para la pila).

Cuando quieras usar cada uno:

  • Las estadísticas / globales son útiles para la memoria que sabes que siempre necesitarás y sabes que no quieres desasignar nunca. (Por cierto, se puede pensar que los entornos integrados solo tienen memoria estática ... la pila y el montón son parte de un espacio de direcciones conocido compartido por un tercer tipo de memoria: el código del programa. Los programas suelen hacer una asignación dinámica de su memoria estática cuando necesitan cosas como listas enlazadas. Pero, independientemente, la memoria estática en sí misma (el búfer) no está "asignada", sino que otros objetos se asignan a la memoria mantenida por el búfer para este fin. Puedes hacerlo también en juegos no integrados, y los juegos de consola con frecuencia evitarán los mecanismos de memoria dinámica integrados a favor de controlar estrictamente el proceso de asignación mediante el uso de almacenamientos intermedios de tamaños preestablecidos para todas las asignaciones).

  • Las variables de pila son útiles para cuando sabe que mientras la función esté dentro del alcance (en la pila en alguna parte), querrá que las variables permanezcan. Las pilas son buenas para las variables que necesita para el código donde están ubicadas, pero que no son necesarias fuera de ese código. También son realmente agradables para cuando accedes a un recurso, como un archivo, y quieres que el recurso desaparezca automáticamente cuando dejas ese código.

  • Las asignaciones de montón (memoria asignada dinámicamente) son útiles cuando desea ser más flexible que el anterior. Con frecuencia, se llama a una función para responder a un evento (el usuario hace clic en el botón "Crear cuadro"). La respuesta adecuada puede requerir la asignación de un nuevo objeto (un nuevo objeto Box) que debería permanecer mucho tiempo después de salir de la función, por lo que no puede estar en la pila. Pero no sabe cuántas cajas desearía al inicio del programa, por lo que no puede ser estático.

Recolección de basura

Últimamente he escuchado muchas cosas sobre lo buenos que son los recolectores de basura, así que tal vez un poco de voz disidente sería útil.

Garbage Collection es un maravilloso mecanismo para cuando el rendimiento no es un gran problema. He oído que los GC son cada vez mejores y más sofisticados, pero el hecho es que puede verse obligado a aceptar una penalización de rendimiento (dependiendo del caso de uso). Y si eres perezoso, puede que aún no funcione correctamente. En el mejor de los casos, los recolectores de basura se dan cuenta de que su memoria desaparece cuando se da cuenta de que ya no hay más referencias (consulte el recuento de referencias ). Pero, si tiene un objeto que se refiere a sí mismo (posiblemente al referirse a otro objeto que hace referencia a él), el recuento de referencia solo no indicará que la memoria se puede eliminar. En este caso, el GC necesita examinar toda la sopa de referencia y descubrir si hay islas a las que solo hagan referencia. A primera vista, supongo que se trata de una operación O (n ^ 2), pero sea lo que sea, puede ser malo si te preocupa el rendimiento. (Editar: Martin B señala que es O (n) para algoritmos razonablemente eficientes. Todavía es O (n) demasiado si le preocupa el rendimiento y puede desasignar en tiempo constante sin recolección de basura.)

Personalmente, cuando escucho que la gente dice que C ++ no tiene recolección de basura, mi mente etiqueta eso como una característica de C ++, pero probablemente soy una minoría. Probablemente, lo más difícil para las personas al aprender sobre programación en C y C ++ son los indicadores y cómo manejar correctamente sus asignaciones de memoria dinámica. Algunos otros lenguajes, como Python, serían horribles sin GC, así que creo que todo se reduce a lo que quieres de un idioma. Si desea un rendimiento confiable, entonces C ++ sin recolección de basura es lo único que se me ocurre de este lado de Fortran. Si desea facilidad de uso y ruedas de entrenamiento (para evitar que se caiga sin que sea necesario que aprenda la administración de memoria "adecuada"), elija algo con un GC. Incluso si sabe cómo administrar bien la memoria, le ahorrará tiempo, lo que puede gastar optimizando otro código. Ya no hay una gran penalización por rendimiento, pero si realmente necesitas un rendimiento confiable (y la capacidad de saber exactamente qué está pasando, cuándo, bajo las sábanas) me quedaría con C ++. Hay una razón por la cual cada motor de juego importante que he escuchado está en C ++ (si no en C o ensamblado). Python, et al están bien para el scripting, pero no para el motor principal del juego.