memoria - punteros c++
¿Qué pasa REALMENTE cuando no se libera después de malloc? (17)
Esto ha sido algo que me ha molestado durante años.
A todos nos enseñan en la escuela (al menos yo lo estaba) que DEBES liberar cada puntero asignado. Sin embargo, estoy un poco curioso sobre el costo real de no liberar la memoria. En algunos casos obvios, como cuando se llama a malloc
dentro de un bucle o parte de la ejecución de un subproceso, es muy importante liberarlo para que no haya pérdidas de memoria. Pero considere los siguientes dos ejemplos:
Primero, si tengo un código que es algo como esto:
int main()
{
char *a = malloc(1024);
/* Do some arbitrary stuff with ''a'' (no alloc functions) */
return 0;
}
¿Cuál es el resultado real aquí? Mi idea es que el proceso muere y luego el espacio de almacenamiento se ha ido de todos modos, por lo que no hay nada de malo en perder la llamada a la free
(sin embargo, sí reconozco la importancia de tenerlo de todos modos para el cierre, la capacidad de mantenimiento y las buenas prácticas). Estoy en lo cierto en este pensamiento?
Segundo, digamos que tengo un programa que actúa un poco como un shell. Los usuarios pueden declarar variables como aaa = 123
y éstas se almacenan en una estructura de datos dinámica para su uso posterior. Claramente, parece obvio que usarías alguna solución que llame a alguna función * alloc (hashmap, lista enlazada, algo así). Para este tipo de programa, no tiene sentido liberarlo nunca después de llamar a malloc
porque estas variables deben estar presentes en todo momento durante la ejecución del programa y no hay una buena forma (que pueda ver) de implementar esto con espacio asignado de forma estática. ¿Es un mal diseño tener un montón de memoria asignada pero solo liberada como parte del proceso que termina? Si es así, ¿cuál es la alternativa?
¿Cuál es el resultado real aquí?
Tu programa filtró la memoria. Dependiendo de su sistema operativo, puede haber sido recuperado.
La mayoría de los sistemas operativos de escritorio modernos recuperan la memoria perdida al finalizar el proceso, por lo que es muy común ignorar el problema, como se puede ver en muchas otras respuestas aquí.
Pero confía en una función de seguridad en la que no debería confiar, y su programa (o función) podría ejecutarse en un sistema en el que este comportamiento podría provocar una pérdida de memoria "dura" la próxima vez.
Es posible que se esté ejecutando en modo kernel, o en sistemas operativos antiguos / integrados que no emplean protección de memoria como una compensación. (Los MMU ocupan espacio en la matriz, la protección de la memoria cuesta ciclos de CPU adicionales y no es mucho pedirle a un programador que limpie después de él).
Puede usar y reutilizar la memoria de la forma que desee, pero asegúrese de haber asignado todos los recursos antes de salir.
Usted está en lo correcto, no se hace daño y es más rápido simplemente salir
Hay varias razones para esto:
Todos los entornos de escritorio y servidor simplemente liberan todo el espacio de memoria en exit (). Desconocen las estructuras de datos internas del programa, como los montones.
Casi todas
free()
implementacionesfree()
nunca devuelven la memoria al sistema operativo de todos modos.Más importante aún, es una pérdida de tiempo cuando se hace justo antes de salir (). En la salida, las páginas de memoria y el espacio de intercambio son simplemente liberados. Por el contrario, una serie de llamadas gratuitas () quemarán el tiempo de CPU y pueden dar como resultado operaciones de paginación del disco, errores de caché y desalojos de caché.
Con respecto a la posibilidad de reutilización de código en el futuro que justifique la certeza de operaciones sin sentido: eso es una consideración, pero podría decirse que no es la forma Agile . YAGNI!
=== ¿Qué pasa con las futuras pruebas y la reutilización del código ? ===
Si no escribe el código para liberar los objetos, entonces está limitando el código a solo ser seguro de usar cuando puede depender de que la memoria se libere por el proceso que se está cerrando ... es decir, un pequeño uso de una sola vez proyectos o proyectos "desechables" [1] ) ... donde sabe cuándo terminará el proceso.
Si escribe el código que libera () toda su memoria asignada dinámicamente, en el futuro estará probando el código y dejando que otros lo utilicen en un proyecto más grande.
[1] sobre proyectos "desechables". El código utilizado en proyectos "desechables" tiene una forma de no ser desechado. Lo siguiente que sabe es que han pasado diez años y su código de "tirar" todavía se está utilizando).
Escuché una historia sobre un tipo que escribió un código solo por diversión para hacer que su hardware funcione mejor. Dijo que " solo un pasatiempo, no será grande y profesional ". Años más tarde, muchas personas están usando su código de "pasatiempo".
Casi todos los sistemas operativos modernos recuperarán todo el espacio de memoria asignado después de que salga un programa. La única excepción que se me ocurre puede ser algo como el sistema operativo Palm, donde el almacenamiento estático y la memoria de tiempo de ejecución del programa son prácticamente lo mismo, por lo que no liberarlo puede hacer que el programa consuma más espacio de almacenamiento. (Sólo estoy especulando aquí.)
Por lo general, no hay daño en ello, excepto el costo de tiempo de ejecución de tener más almacenamiento del que necesita. Ciertamente, en el ejemplo que da, desea conservar la memoria para una variable que podría usarse hasta que se borre.
Sin embargo, se considera un buen estilo para liberar la memoria tan pronto como ya no la necesite, y para liberar cualquier cosa que aún tenga al salir del programa. Es más un ejercicio para saber qué memoria está usando y pensar si todavía la necesita. Si no realiza un seguimiento, es posible que tenga pérdidas de memoria.
Por otro lado, la advertencia similar de cerrar los archivos al salir tiene un resultado mucho más concreto: si no lo hace, es posible que los datos que les escribió no se eliminen, o si son un archivo temporal, es posible que no se eliminen. ser eliminado cuando haya terminado. Además, los manejadores de la base de datos deben tener sus transacciones comprometidas y luego cerrarse cuando haya terminado con ellas. De manera similar, si está utilizando un lenguaje orientado a objetos como C ++ o Objective C, no liberar un objeto cuando termine con él significará que nunca se llamará al destructor, y que los recursos de los que la clase es responsable podrían no ser limpiados.
En realidad, hay una sección en el libro de texto en línea de OSTEP para un curso de licenciatura en sistemas operativos que discute exactamente tu pregunta.
La sección relevante es "Forgetting To Free Memory" en el capítulo de la API de memoria en la página 6 que brinda la siguiente explicación:
En algunos casos, puede parecer que no llamar gratis () es razonable. Por ejemplo, su programa es de corta duración y pronto saldrá; en este caso, cuando el proceso finalice, el sistema operativo limpiará todas las páginas asignadas y, por lo tanto, no se producirá ninguna pérdida de memoria en sí. Si bien esto ciertamente “funciona” (vea el apartado en la página 7), es probable que se desarrolle un mal hábito, así que tenga cuidado al elegir una estrategia de este tipo.
Este extracto es en el contexto de la introducción del concepto de memoria virtual. Básicamente, en este punto del libro, los autores explican que uno de los objetivos de un sistema operativo es "virtualizar la memoria", es decir, dejar que cada programa crea que tiene acceso a un espacio de direcciones de memoria muy grande.
Detrás de escena, el sistema operativo traducirá las "direcciones virtuales" que el usuario ve a las direcciones reales que apuntan a la memoria física.
Sin embargo, compartir recursos como la memoria física requiere que el sistema operativo realice un seguimiento de los procesos que lo utilizan. Entonces, si un proceso termina, entonces está dentro de las capacidades y los objetivos de diseño del sistema operativo recuperar la memoria del proceso para que pueda redistribuir y compartir la memoria con otros procesos.
EDITAR: El aparte mencionado en el extracto se copia a continuación.
A PARTIR DE: POR QUÉ NO SE PIERDE UNA MEMORIA UNA VEZ QUE EL PROCESO SE SALE
Cuando escribes un programa de corta duración, puedes asignar algo de espacio usando
malloc()
. El programa se ejecuta y está a punto de completarse: ¿es necesario llamarfree()
un montón de veces justo antes de salir? Aunque parezca incorrecto no hacerlo, ningún recuerdo se "perderá" en ningún sentido real. La razón es simple: en realidad hay dos niveles de administración de memoria en el sistema. El primer nivel de gestión de la memoria lo realiza el sistema operativo, que distribuye la memoria a los procesos cuando se ejecutan, y la recupera cuando los procesos salen (o mueren). El segundo nivel de administración está dentro de cada proceso, por ejemplo, dentro del montón cuando llama amalloc()
yfree()
. Incluso si no puede llamar afree()
(y, por tanto, pierde memoria en el montón), el sistema operativo recuperará toda la memoria del proceso (incluidas las páginas para código, pila y, según corresponda aquí, montón) cuando el programa se terminó de correr. No importa cuál sea el estado de su montón en su espacio de direcciones, el sistema operativo recupera todas esas páginas cuando el proceso muere, lo que garantiza que no se pierda la memoria a pesar del hecho de que no la liberó.Por lo tanto, para los programas de corta duración, la pérdida de memoria a menudo no causa ningún problema operacional (aunque puede considerarse una forma deficiente). Cuando escribe un servidor de larga duración (como un servidor web o un sistema de administración de bases de datos, que nunca se cierra), la memoria perdida es un problema mucho más grande y, eventualmente, se bloquea cuando la aplicación se queda sin memoria. Y, por supuesto, la pérdida de memoria es un problema aún mayor dentro de un programa en particular: el propio sistema operativo. Mostrándonos una vez más: los que escriben el código del kernel tienen el trabajo más difícil de todos ...
de la página 7 del capítulo API de memoria de
OSTEP
Remzi H. Arpaci-Dusseau y Andrea C. Arpaci-Dusseau Libros de Arpaci-Dusseau Marzo de 2015 (Versión 0.90)
Está completamente bien dejar la memoria sin cargar al salir; malloc () asigna la memoria desde el área de memoria llamada "el montón", y el montón completo de un proceso se libera cuando el proceso termina.
Dicho esto, una de las razones por las que la gente sigue insistiendo en que es bueno liberar todo antes de salir es que los depuradores de memoria (por ejemplo, valgrind en Linux) detectan los bloques no liberados como pérdidas de memoria, y si usted también tiene pérdidas de memoria "reales", se convierte en Más difícil de detectar si también obtienes resultados "falsos" al final.
Este código generalmente funcionará bien, pero considere el problema de la reutilización del código.
Es posible que haya escrito algún fragmento de código que no libera la memoria asignada, se ejecuta de tal manera que la memoria se recupera automáticamente. Parece bien.
Luego, alguien más copia su fragmento en su proyecto de tal manera que se ejecuta mil veces por segundo. Esa persona ahora tiene una gran pérdida de memoria en su programa. No muy bueno en general, generalmente fatal para una aplicación de servidor.
La reutilización de códigos es típica en las empresas. Por lo general, la compañía posee todo el código que producen sus empleados y cada departamento puede reutilizar lo que sea que posea. Por lo tanto, al escribir un código de "apariencia inocente" puede causar un posible dolor de cabeza a otras personas. Esto puede hacer que te despidan.
Estoy completamente en desacuerdo con todos los que dicen que OP es correcto o que no hay daño.
Todo el mundo habla de sistemas operativos modernos y / o heredados.
Pero, ¿y si estoy en un entorno donde simplemente no tengo SO? Donde no hay nada?
Imagina que ahora estás usando interrupciones de estilo de hilos y asigna memoria. En la norma C / ISO / IEC: 9899 es la vida útil de la memoria indicada como:
7.20.3 Funciones de gestión de memoria
1 El orden y la continuidad del almacenamiento asignado por las llamadas sucesivas a las funciones calloc, malloc y realloc no están especificados. El puntero devuelto si la asignación es exitosa se alinea adecuadamente para que pueda ser asignado a un puntero a cualquier tipo de objeto y luego se use para acceder a dicho objeto o una matriz de dichos objetos en el espacio asignado (hasta que el espacio esté desasignado explícitamente) . La vida útil de un objeto asignado se extiende desde la asignación hasta la desasignación. [...]
Por lo tanto, no se debe tener en cuenta que el entorno está haciendo el trabajo de liberación para usted. De lo contrario, se agregaría a la última frase: "O hasta que el programa finalice".
En otras palabras, no liberar la memoria no es solo una mala práctica. Produce códigos no portátiles y no conformes a C. Que al menos puede verse como ''correcto, si lo siguiente: [...] es compatible con el entorno''.
Pero en los casos en los que no tiene ningún sistema operativo, nadie está haciendo el trabajo por usted (sé que generalmente no asigna ni reasigna memoria en los sistemas integrados, pero hay casos en los que puede desear).
Por lo tanto, hablando en general C simple (como está etiquetado el OP), esto simplemente está produciendo código erróneo y no portátil.
No hay peligro real en no liberar sus variables, pero si asigna un puntero a un bloque de memoria a un bloque de memoria diferente sin liberar el primer bloque, el primer bloque ya no es accesible pero aún ocupa espacio. Esto es lo que se denomina una pérdida de memoria, y si lo hace con regularidad, su proceso comenzará a consumir más y más memoria, eliminando los recursos del sistema de otros procesos.
Si el proceso es de corta duración, a menudo puede hacer esto, ya que el sistema operativo recupera toda la memoria asignada cuando finaliza el proceso, pero le aconsejaría que adquiera el hábito de liberar toda la memoria para la que ya no tenga uso.
Normalmente libero cada bloque asignado una vez que estoy seguro de que he terminado con él. Hoy, el punto de entrada de mi programa podría ser main(int argc, char *argv[])
, pero mañana podría ser foo_entry_point(char **args, struct foo *f)
y se escribió como un puntero a función.
Entonces, si eso sucede, ahora tengo una fuga.
Con respecto a su segunda pregunta, si mi programa tomara información como a = 5, asignaría espacio para a, o reasignaría el mismo espacio en un subsiguiente a = "foo". Esto quedaría asignado hasta:
- El usuario escribió ''unset''
- Se ingresó a mi función de limpieza, ya sea para dar servicio a una señal o el usuario escribió "salir"
No puedo pensar en ningún sistema operativo moderno que no recupere la memoria después de un proceso de salida. Por otra parte, gratis () es barato, ¿por qué no limpiar? Como han dicho otros, las herramientas como valgrind son excelentes para detectar fugas por las que realmente debes preocuparte. A pesar de que los bloques de su ejemplo se etiquetarían como ''aún accesibles'', es solo un ruido extra en la salida cuando intenta asegurarse de que no haya fugas.
Otro mito es " Si está en main (), no tengo que liberarlo ", esto es incorrecto. Considera lo siguiente:
char *t;
for (i=0; i < 255; i++) {
t = strdup(foo->name);
let_strtok_eat_away_at(t);
}
Si eso ocurrió antes de forking / daemonizing (y, en teoría, se ejecuta para siempre), su programa acaba de filtrar un tamaño indeterminado de t 255 veces.
Un programa bueno y bien escrito siempre debe limpiarse después de sí mismo. Libere toda la memoria, vacíe todos los archivos, cierre todos los descriptores, desvincule todos los archivos temporales, etc. Esta función de limpieza debe alcanzarse en la terminación normal, o al recibir varios tipos de señales fatales, a menos que desee dejar algunos archivos por ahí para que pueda detectar un accidente y reanudar
De verdad, sé amable con la pobre alma que tiene que mantener tus cosas cuando pasas a otras cosas ... dáselas ''valgrind clean'' :)
Sí, tienes razón, tu ejemplo no hace ningún daño (al menos no en la mayoría de los sistemas operativos modernos). Toda la memoria asignada por su proceso será recuperada por el sistema operativo una vez que el proceso finalice.
Fuente: Asignación y mitos de GC (alerta de PostScript!)
Mito de asignación 4: los programas que no se recolectan con basura siempre deben desasignar toda la memoria que asignan.
La verdad: las desasignaciones omitidas en el código ejecutado con frecuencia causan fugas crecientes. Rara vez son aceptables. pero los programas que retienen la mayor parte de la memoria asignada hasta la salida del programa a menudo tienen un mejor desempeño sin ninguna intervención intermedia. Malloc es mucho más fácil de implementar si no hay libre.
En la mayoría de los casos, desasignar la memoria justo antes de salir del programa no tiene sentido. El sistema operativo lo recuperará de todos modos. Voluntad libre y página en los objetos muertos; el sistema operativo no lo hará.
Consecuencia: Tenga cuidado con los "detectores de fugas" que cuentan las asignaciones. ¡Algunas "fugas" son buenas!
Dicho esto, ¡realmente deberías intentar evitar todas las fugas de memoria!
Segunda pregunta: tu diseño está bien. Si necesita almacenar algo hasta que la aplicación salga, está bien hacer esto con la asignación de memoria dinámica. Si no conoce el tamaño requerido por adelantado, no puede usar la memoria asignada estáticamente.
Si está desarrollando una aplicación desde cero, puede tomar decisiones informadas sobre cuándo llamar gratuitamente. Su programa de ejemplo está bien: asigna memoria, tal vez lo tiene funcionando por unos segundos y luego se cierra, liberando todos los recursos que reclamó.
Sin embargo, si está escribiendo algo más: un servidor / aplicación de larga duración, o una biblioteca para ser utilizada por otra persona, debe esperar llamar gratis a todo lo que haga.
Ignorar el lado pragmático por un segundo, es mucho más seguro seguir un enfoque más estricto y obligarte a liberar todo lo que quieras. Si no tiene la costumbre de observar las fugas de memoria cada vez que codifica, podría fácilmente saltar algunas fugas. Entonces, en otras palabras, sí, puedes escapar sin eso; por favor ten cuidado, sin embargo
Si está utilizando la memoria que ha asignado, entonces no está haciendo nada malo. Se convierte en un problema cuando se escriben funciones (distintas de las principales) que asignan memoria sin liberarla y sin que esté disponible para el resto de su programa. Entonces su programa continúa ejecutándose con esa memoria asignada, pero no hay forma de usarlo. Su programa y otros programas en ejecución están privados de esa memoria.
Edición: no es 100% exacto decir que otros programas en ejecución están privados de esa memoria. El sistema operativo siempre puede permitir que lo utilicen a expensas de cambiar su programa a la memoria virtual ( </handwaving>
). Sin embargo, el punto es que si su programa libera memoria que no está utilizando, es menos probable que sea necesario un intercambio de memoria virtual.
Usted es absolutamente correcto en ese sentido. En pequeños programas triviales donde una variable debe existir hasta la muerte del programa, no hay un beneficio real para desasignar la memoria.
De hecho, una vez estuve involucrado en un proyecto en el que cada ejecución del programa era muy compleja pero de corta duración, y la decisión fue simplemente mantener la memoria asignada y no desestabilizar el proyecto al cometer errores en su ubicación.
Dicho esto, en la mayoría de los programas esto no es realmente una opción, o puede llevarlo a quedarse sin memoria.
Usted es correcto, la memoria se libera automáticamente cuando sale el proceso. Algunas personas se esfuerzan por no realizar una limpieza exhaustiva cuando finaliza el proceso, ya que todo se entregará al sistema operativo. Sin embargo, mientras el programa se está ejecutando, debe liberar memoria no utilizada. Si no lo hace, puede quedarse sin tiempo o causar una paginación excesiva si su conjunto de trabajo se vuelve demasiado grande.
Creo que sus dos ejemplos son en realidad uno solo: free()
debería ocurrir solo al final del proceso, lo cual, como usted señala, es inútil ya que el proceso está finalizando.
Sin embargo, en su segundo ejemplo, la única diferencia es que permite un número indefinido de malloc()
, lo que podría llevar a quedarse sin memoria. La única forma de manejar la situación es verificar el código de retorno malloc()
y actuar en consecuencia.
Si un programa se olvida de liberar algunos megabytes antes de que salga, el sistema operativo los liberará. Pero si su programa se ejecuta durante semanas a la vez y un bucle dentro del programa se olvida de liberar algunos bytes en cada iteración, tendrá una gran pérdida de memoria que consumirá toda la memoria disponible en su computadora a menos que la reinicie de forma regular. basis => incluso las pequeñas pérdidas de memoria pueden ser malas si el programa se usa para una tarea realmente grande, incluso si originalmente no fue diseñado para una.