C ++ eliminar vs Java GC

8

La recolección de basura de Java se encarga de los objetos muertos en el montón, pero a veces congela el mundo. En C ++ tengo que llamar deletepara disponer de un objeto creado al final de su ciclo de vida.

Esto deleteparece un precio muy bajo a pagar por un entorno sin congelación. Colocar todas las deletepalabras clave relevantes es una tarea mecánica. Uno puede escribir un script que recorra el código y elimine una vez que ninguna rama nueva use un objeto dado.

Entonces, ¿cuáles son los pros y los contras de la compilación de Java en el modelo diy C ++ de recolección de basura?


No quiero iniciar un hilo C ++ vs Java. Mi pregunta es diferente
Todo esto del GC: ¿se reduce a "solo estar ordenado, no te olvides de eliminar los objetos que has creado, y no necesitarás ningún GC dedicado? ¿O es más como" deshacerse de los objetos en C ++ es realmente complicado? pasar el 20% de mi tiempo en ello y, sin embargo, las pérdidas de memoria son un lugar común "?

sesenta árboles
fuente
15
Me resulta difícil creer que tu guión mágico pueda existir. Aparte de cualquier cosa, no tendría que contener una solución al problema de detención. O alternativamente (y más probablemente) solo funciona para programas increíblemente simples
Richard Tingle
1
También el Java moderno no se congela, excepto en condiciones extremas donde se crean grandes cantidades de basura
Richard Tingle
1
@ThomasCarlisle bien puede, y ocasionalmente lo hace, afectar el rendimiento. Pero hay muchos parámetros para ajustar en estos casos, y a veces la solución puede ser cambiar a un gc completamente diferente. Todo depende de la cantidad de recursos disponibles y la carga típica.
Hulk
44
" Colocar todas las palabras clave de eliminación relevantes es una tarea mecánica ". Por eso hay herramientas para detectar pérdidas de memoria. Porque es muy simple y no es propenso a errores.
JensG
2
@Doval, si está utilizando RAII correctamente, lo cual es bastante simple, casi no hay contabilidad en absoluto. newentra en el constructor de tu clase que gestiona la memoria, deleteentra en el destructor. A partir de ahí, todo es automático (almacenamiento). NB que esto funciona para todo tipo de recursos, no solo memoria, a diferencia de la recolección de basura. Los mutexes se toman en un constructor, se liberan en un destructor. Los archivos se abren en un constructor, se cierran en un destructor.
Rob K

Respuestas:

18

El ciclo de vida del objeto C ++

Si crea objetos locales, no necesita eliminarlos: el compilador genera código para eliminarlos automáticamente cuando el objeto se sale del alcance

Si usa punteros de objetos y crea objetos en la tienda gratuita, debe asegurarse de eliminar el objeto cuando ya no sea necesario (como ha descrito). Desafortunadamente, en un software complejo esto podría ser mucho más desafiante de lo que parece (por ejemplo, ¿qué pasa si se produce una excepción y nunca se alcanza la parte de eliminación esperada?).

Afortunadamente, en C ++ moderno (también conocido como C ++ 11 y posterior), tiene punteros inteligentes, como por ejemplo shared_ptr. Cuentan por referencia el objeto creado de una manera muy eficiente, un poco como lo haría un recolector de basura en Java. Y tan pronto como el objeto ya no esté referenciado, el último activo shared_ptrelimina el objeto por usted. Automáticamente. Al igual que el recolector de basura, pero un objeto a la vez y sin demora (Ok: necesita un cuidado adicional y weak_ptrhacer frente a las referencias circulares).

Conclusión: hoy en día puede escribir código C ++ sin tener que preocuparse por la asignación de memoria, y que es tan libre de fugas como con un GC, pero sin el efecto de congelación.

El ciclo de vida del objeto Java

Lo bueno es que no tiene que preocuparse por el ciclo de vida de los objetos. Simplemente los creas y Java se encarga del resto. Un GC moderno identificará y destruirá los objetos que ya no son necesarios (incluso si hay referencias circulares entre objetos muertos).

Desafortunadamente, debido a esta comodidad, no tiene un control real de cuándo se elimina realmente el objeto . Semánticamente, la eliminación / destrucción coincide con la recolección de basura .

Esto está perfectamente bien si mira los objetos solo en términos de memoria. Excepto por el congelamiento, pero estos no son una fatalidad (la gente está trabajando en esto). No soy un experto en Java, pero creo que la destrucción retrasada hace que sea más difícil identificar fugas en Java debido a referencias guardadas accidentalmente a pesar de que los objetos ya no son necesarios (es decir, no se puede controlar la eliminación de objetos).

Pero, ¿qué pasa si el objeto tiene que controlar otros recursos además de la memoria, por ejemplo, un archivo abierto, un semáforo, un servicio del sistema? Su clase debe proporcionar un método para liberar estos recursos. Y tendrá la responsabilidad de asegurarse de que se llame a este método cuando los recursos ya no sean necesarios. En cada ruta de ramificación posible a través de su código, asegúrese de que también se invoque en caso de excepciones. El desafío es muy similar a la eliminación explícita en C ++.

Conclusión: el GC resuelve un problema de administración de memoria. Pero no aborda la gestión de otros recursos del sistema. La ausencia de la eliminación "justo a tiempo" podría hacer que la gestión de recursos sea muy difícil.

Eliminación, recolección de basura y RAII

Cuando puede controlar la eliminación de un objeto y el destructor que se va a invocar en la eliminación, puede beneficiarse de la RAII . Este enfoque considera la memoria solo como un caso especial de asignación de recursos y vincula la gestión de recursos de manera más segura al ciclo de vida del objeto, lo que garantiza un uso estrictamente controlado de los recursos.

Christophe
fuente
3
La belleza de los recolectores de basura (modernos) es que realmente no necesita pensar en referencias cíclicas. Tales grupos de objetos que son inalcanzables, excepto entre sí, se detectan y se recopilan. Esa es una gran ventaja sobre el conteo simple de referencias / punteros inteligentes.
Hulk
66
+1 La parte de "excepciones" no se puede enfatizar lo suficiente. La presencia de excepciones hace que la tarea difícil e inútil de la administración manual de la memoria sea prácticamente imposible y, por lo tanto, la administración manual de la memoria no se usa en C ++. Utiliza RAII. No lo use newfuera de constructores / punteros inteligentes, y nunca lo use deletefuera de un destructor.
Felix Dombek
@Hulk Tienes un punto aquí! Aunque la mayoría de mis argumentos siguen siendo válidos, GC hizo muchos progresos. Y las referencias circulares son realmente difíciles de manejar con referencias que cuentan solo con punteros inteligentes. Así que edité mi respuesta en consecuencia, para mantener un mejor equilibrio. También agregué una referencia a un artículo sobre posibles estrategias para mitigar el efecto de congelación.
Christophe
3
+1 La gestión de recursos distintos de la memoria requiere un esfuerzo adicional para los lenguajes GC porque RAII no funciona si no hay un control detallado sobre cuándo (o si) la eliminación / finalización de un objeto sucede. Las construcciones especiales están disponibles en la mayoría de estos lenguajes (ver , por ejemplo, los recursos de prueba de Java ), que pueden, en combinación con las advertencias del compilador, ayudar a corregir estas cosas.
Hulk
1
Like garbage collector, but one object at a time and without delay (Ok: you need some extra care and weak_ptr to cope with circular references).Sin embargo, el recuento de referencias puede caer en cascada. Por ejemplo, la última referencia a Adesaparece, pero Atambién tiene la última referencia a B, quién tiene la última referencia a C... And you'll have the responsibility to make sure that this method is called when the resources are no longer needed. In every possible branching path through your code, ensuring it is also invoked in case of exceptions.Java y C # tienen instrucciones de bloque especiales para esto.
Doval
5

Esta eliminación parece un precio muy bajo a pagar por un entorno sin congelación. Colocar todas las palabras clave de eliminación relevantes es una tarea mecánica. Uno puede escribir un script que recorra el código y elimine una vez que ninguna rama nueva use un objeto dado.

Si puedes escribir un guión como ese, felicidades. Eres un mejor desarrollador que yo. Con mucho.

La única forma de evitar pérdidas de memoria en casos prácticos es mediante estándares de codificación muy estrictos con reglas muy estrictas sobre quién es el propietario de un objeto y cuándo puede y debe liberarse, o herramientas como punteros inteligentes que cuentan referencias a objetos y eliminan objetos cuando la última referencia se ha ido.

gnasher729
fuente
44
Sí, la gestión manual de recursos siempre es propensa a errores, afortunadamente solo es necesario en Java (para recursos sin memoria), ya que C ++ tiene RAII.
Deduplicador
La única forma en que puede evitar CUALQUIER pérdida de recursos en casos prácticos es con estándares de codificación muy estrictos con reglas muy estrictas quién es el propietario de un objeto ... la memoria no es el único recurso, y en la mayoría de los casos, no el más crítico ya sea.
curioso
2
Si considera que RAII es un "estándar de codificación muy estricto". Lo considero un 'pozo de éxito', trivialmente fácil de usar.
Rob K
5

Si escribe el código C ++ correcto con RAII, generalmente no escribe ningún nuevo o borrado. Los únicos "nuevos" que escribes están dentro de punteros compartidos, por lo que realmente nunca tienes que usar "eliminar".

Nikko
fuente
1
No debería usarlo newen absoluto, incluso con punteros compartidos, debería usarlo en su std::make_sharedlugar.
Jules
1
o mejor, make_unique. Es realmente bastante raro que realmente necesites una propiedad compartida.
Marc
4

Hacer la vida de los programadores más fácil y evitar pérdidas de memoria es una ventaja importante de la recolección de basura, pero no es la única. Otro es prevenir la fragmentación de la memoria. En C ++, una vez que asigna un objeto usando la newpalabra clave, permanece en una posición fija en la memoria. Esto significa que, a medida que se ejecuta la aplicación, terminas teniendo espacios libres de memoria entre los objetos asignados. Por lo tanto, la asignación de memoria en C ++ debe ser necesariamente un proceso más complicado, ya que el sistema operativo debe poder encontrar bloques no asignados de un tamaño determinado que se ajusten entre los espacios.

La recolección de basura se encarga de tomar todos los objetos que no se eliminan y desplazarlos en la memoria para que formen un bloque continuo. Si experimenta que la recolección de basura lleva algún tiempo, probablemente se deba a este proceso, no a la desasignación de memoria en sí. El beneficio de esto es que cuando se trata de asignación de memoria, es casi tan sencillo como mover un puntero al final de la pila.

Entonces, en C ++, eliminar objetos es rápido, pero crearlos puede ser lento. En Java, la creación de objetos no lleva tiempo, pero debe realizar algunas tareas de limpieza de vez en cuando.

kamilk
fuente
44
Sí, la asignación desde la tienda gratuita es más lenta en C ++ que Java. Afortunadamente, es mucho menos frecuente, y puede usar fácilmente asignadores de propósito especial a su propia discreción cuando tenga patrones inusuales. Además, todos los recursos son iguales en C ++, Java tiene los casos especiales.
Deduplicador
3
En 20 años de codificación en C ++, nunca he visto que la fragmentación de la memoria se convierta en un problema. Los sistemas operativos modernos con administración de memoria virtual en procesadores con múltiples niveles de caché han eliminado en gran medida esto como un problema.
Rob K
@RobK: "Los sistemas operativos modernos con administración de memoria virtual en procesadores con múltiples niveles de caché han eliminado en gran medida [la fragmentación] como un problema", no, no lo han hecho. Es posible que no lo note, pero aún sucede, aún causa (1) memoria desperdiciada y (2) uso menos eficiente de cachés, y las únicas soluciones viables son copiar GC o un diseño muy cuidadoso de la administración manual de memoria para garantizar que no No suceda
Jules
3

Las principales promesas de Java fueron

  1. Sintaxis comprensible como C
  2. Escribe una carrera en todas partes
  3. Hacemos su trabajo más fácil, incluso nos ocupamos de la basura.

Parece que Java le garantiza que la basura se eliminará (no necesariamente de manera eficiente). Si usa C / C ++, tiene libertad y responsabilidad. Puede hacerlo mejor que el GC de Java, o puede ser mucho peor (omita deletetodo junto y tenga problemas de pérdida de memoria).

Si necesita un código que "cumpla con ciertos estándares de calidad" y para optimizar la "relación precio / calidad", use Java. Si está listo para invertir recursos adicionales (tiempo de sus expertos) para mejorar el rendimiento de la aplicación de misión crítica, use C.

Ju Shua
fuente
Bueno, todas las promesas pueden verse como rotas, fácilmente.
Deduplicador
Excepto que la única basura que Java recolectará es la memoria asignada dinámicamente. No hace nada sobre ningún otro recurso asignado dinámicamente.
Rob K
@RobK: eso no es estrictamente cierto. El uso de finalizadores de objetos puede manejar la desasignación de otros recursos. Se desaconseja ampliamente porque en la mayoría de los casos no desea eso (a diferencia de la memoria, la mayoría de los otros tipos de recursos son mucho más limitados o incluso únicos y, por lo tanto, la desasignación precisa es crítica), pero se puede hacer. Java también tiene la declaración de prueba con recursos que se puede utilizar para automatizar la administración de otros recursos (proporcionando beneficios similares a RAII).
Jules
@Jules: Una diferencia clave es que en los objetos C ++ se autodestruyen. En Java, los objetos que se pueden cerrar no se pueden cerrar. Un desarrollador de Java que escribe "algo nuevo (...)" o (peor) algo s = SomeFactory.create (...) debe verificar si algo es cerrable. Cambiar una clase de no Cerrable a Cerrable es el peor tipo de cambio de ruptura, por lo que prácticamente nunca se puede hacer. No es tan malo a nivel de clase, pero es un problema grave cuando se define una interfaz Java.
Kevin Cline
2

La gran diferencia que hace la recolección de basura no es que no tenga que eliminar objetos explícitamente. La diferencia mucho mayor es que no tienes que copiar objetos.

Esto tiene efectos que se generalizan en el diseño de programas e interfaces en general. Permítanme dar un pequeño ejemplo para mostrar cuán de largo alcance es esto.

En Java, cuando saca algo de una pila, se devuelve el valor que se está sacando, por lo que obtiene un código como este:

WhateverType value = myStack.Pop();

En Java, esto es una excepción segura, porque todo lo que realmente estamos haciendo es copiar una referencia a un objeto, lo que se garantiza que sucederá sin una excepción. Sin embargo, lo mismo no es cierto en C ++. En C ++, devolver un valor significa (o al menos puede significar) copiar ese valor, y con algunos tipos que podrían generar una excepción. Si la excepción se produce después de que el elemento se haya eliminado de la pila, pero antes de que la copia llegue al receptor, el elemento se ha filtrado. Para evitar eso, la pila de C ++ utiliza un enfoque algo más torpe donde recuperar el elemento superior y eliminar el elemento superior son dos operaciones separadas:

WhateverType value = myStack.top();
myStack.pop();

Si la primera declaración arroja una excepción, la segunda no se ejecutará, por lo que si se lanza una excepción al copiar, el elemento permanece en la pila como si nada hubiera sucedido.

El problema obvio es que esto es simplemente torpe y (para las personas que no lo han usado) inesperado.

Lo mismo es cierto en muchas otras partes de C ++: especialmente en el código genérico, la seguridad de excepción impregna muchas partes del diseño, y esto se debe en gran parte al hecho de que la mayoría al menos potencialmente implica copiar objetos (que podrían arrojarse), donde Java simplemente crearía nuevas referencias a objetos existentes (que no pueden arrojarse, por lo que no tenemos que preocuparnos por las excepciones).

En cuanto a una secuencia de comandos simple para insertar deletedonde sea necesario: si puede determinar estáticamente cuándo eliminar elementos en función de la estructura del código fuente, probablemente no debería haber estado utilizando newy, deleteen primer lugar.

Permíteme darte un ejemplo de un programa para el cual esto seguramente no sería posible: un sistema para hacer, rastrear, facturar (etc.) llamadas telefónicas. Cuando marca su teléfono, crea un objeto de "llamada". El objeto de llamada realiza un seguimiento de a quién llamó, cuánto tiempo hable con ellos, etc., para agregar registros apropiados a los registros de facturación. El objeto de llamada monitorea el estado del hardware, por lo que cuando cuelga, se destruye a sí mismo (usando el ampliamente discutido delete this;). Solo que no es tan trivial como "cuando cuelgas". Por ejemplo, puede iniciar una llamada de conferencia, conectar a dos personas y colgar, pero la llamada continúa entre esas dos partes incluso después de que cuelgue (pero la facturación puede cambiar).

Jerry Coffin
fuente
"Si se produce la excepción después de que el elemento se haya eliminado de la pila, pero antes de que la copia llegue al receptor, el elemento se ha filtrado" ¿tiene alguna referencia para esto? Porque esto suena extremadamente extraño aunque no soy un experto en c ++.
Esben Skov Pedersen
@EsbenSkovPedersen: GoTW # 8 sería un punto de partida razonable. Si tiene acceso a él, Excepcional C ++ tiene bastante más. Tenga en cuenta que ambos esperan al menos un conocimiento preexistente de C ++.
Jerry Coffin
Eso parece bastante sencillo. En realidad, es esta oración la que me confundió "En C ++, devolver un valor significa (o al menos puede significar) copiar ese valor, y con algunos tipos que podrían arrojar una excepción" ¿Esta copia está en el montón o pila?
Esben Skov Pedersen
A pesar de todo no tiene que copiar objetos en C ++, pero puede hacerlo si lo desea, a diferencia de los lenguajes de recogida de basura (Java, C #) que lo convierten en un PITA para copiar un objeto cuando se desea. El 90% de los objetos que creo deben ser destruidos y sus recursos liberados cuando salgan del alcance. Para forzar todos los objetos al almacenamiento dinámico porque el 10% de ellos deben parecer tontos en el mejor de los casos.
Rob K
2
Sin embargo, esto no es realmente una diferencia causada por el uso de la recolección de basura, sino por la filosofía simplificada de Java de "todos los objetos son referencias". Considere C # como un contraejemplo: es un lenguaje recolectado de basura, pero también tiene objetos de valor ("estructuras" en la terminología local, que son diferentes de las estructuras de C ++) que tienen una semántica de copia. C # evita el problema al (1) tener una separación clara entre los tipos de referencia y valor y (2) siempre copiando los tipos de valor utilizando la copia byte por byte, no el código del usuario, evitando así excepciones durante la copia.
Jules
2

Algo que no creo que se haya mencionado aquí es que existen eficiencias que provienen de la recolección de basura. En los recopiladores Java más utilizados, el lugar principal donde se asignan los objetos es un área reservada para un recopilador de copia. Cuando las cosas comienzan, este espacio está vacío. A medida que se crean los objetos, se asignan uno al lado del otro en el gran espacio abierto hasta que no pueda asignar uno en el espacio contiguo restante. El GC se activa y busca cualquier objeto en este espacio que no esté muerto. Copia los objetos vivos a otra área y los junta (es decir, sin fragmentación). El espacio antiguo se considera limpio. Luego continúa asignando objetos estrechamente juntos y repite este proceso según sea necesario.

Hay dos beneficios para esto. La primera es que no se dedica tiempo a eliminar los objetos no utilizados. Una vez que se copian los objetos vivos, la pizarra se considera limpia y los objetos muertos simplemente se olvidan. En muchas aplicaciones, la mayoría de los objetos no viven mucho tiempo, por lo que el costo de copiar el conjunto en vivo es económico en comparación con los ahorros obtenidos al no tener que preocuparse por el conjunto muerto.

El segundo beneficio es que cuando se asigna un nuevo objeto, no hay necesidad de buscar un área contigua. La VM siempre sabe dónde se colocará el siguiente objeto (advertencia: simplificado ignorando la concurrencia).

Este tipo de recolección y asignación es muy rápido. Desde una perspectiva de rendimiento general, es difícil de superar para muchos escenarios. El problema es que algunos objetos van a vivir más tiempo del que desea seguir copiando y, en última instancia, eso significa que el recolector puede tener que hacer una pausa durante un período de tiempo significativo de vez en cuando y cuando eso suceda puede ser impredecible. Dependiendo de la duración de la pausa y el tipo de aplicación, esto puede o no ser un problema. Hay al menos un colector sin pausa . Espero que haya una compensación de menor eficiencia para obtener la naturaleza sin pausa, pero una de las personas que fundaron esa compañía (Gil Tene) es un súper experto en GC y sus presentaciones son una gran fuente de información sobre GC.

JimmyJames
fuente
Azul fue fundado por un grupo de chicos de GC, compiladores, rendimiento y ex CPU Java de Sun. Ellos saben lo que están haciendo.
Jörg W Mittag
@ JörgWMittag se actualizó para reflejar que había más de un fundador. gracias
JimmyJames
1

¿O es más como "deshacerse de objetos en C ++ es realmente complicado: paso el 20% de mi tiempo en él y, sin embargo, las pérdidas de memoria son un lugar común"?

En mi experiencia personal en C ++ e incluso C, las pérdidas de memoria nunca han sido una gran lucha para evitar. Con un procedimiento de prueba sensato y Valgrind, por ejemplo, cualquier fuga física causada por una llamada operator new/mallocsin un correspondiente a delete/freemenudo se detecta y repara rápidamente. Para ser justos, algunas bases de códigos C ++ grandes de C o de la vieja escuela podrían tener algunos casos extremos oscuros que podrían perder físicamente algunos bytes de memoria aquí y allá como resultado de no estar deleting/freeingen ese caso límite que pasó desapercibido.

Sin embargo, en lo que respecta a las observaciones prácticas, las aplicaciones con más fugas que encuentro (como en las que consumen más y más memoria cuanto más las ejecutas, a pesar de que la cantidad de datos con los que estamos trabajando no está creciendo) generalmente no se escriben en C o C ++. No encuentro cosas como el Kernel de Linux o Unreal Engine o incluso el código nativo utilizado para implementar Java entre la lista de software con fugas que encuentro.

El tipo más destacado de software con fugas que tiendo a encontrar son cosas como los applets de Flash, como los juegos de Flash, a pesar de que usan la recolección de basura. Y esa no es una comparación justa si se dedujera algo de esto, ya que muchas aplicaciones Flash están escritas por desarrolladores incipientes que probablemente carecen de principios de ingeniería y procedimientos de prueba sólidos (y del mismo modo estoy seguro de que hay profesionales capacitados que trabajan con GC que no luche con el software con fugas), pero tendría mucho que decir a cualquiera que piense que GC evita que se escriba el software con fugas.

Punteros colgantes

Ahora, viniendo de mi dominio particular, experiencia, y como uno que usa principalmente C y C ++ (y espero que los beneficios de GC varíen según nuestras experiencias y necesidades), lo más inmediato que GC resuelve para mí no son problemas prácticos de pérdida de memoria, sino cuelga el acceso del puntero, y eso podría ser literalmente un salvavidas en escenarios de misión crítica.

Desafortunadamente, en muchos de los casos en que GC resuelve lo que de otro modo sería un acceso de puntero colgante, reemplaza el mismo tipo de error del programador con una pérdida de memoria lógica.

Si imagina ese juego Flash escrito por un codificador en ciernes, podría almacenar referencias a elementos del juego en múltiples estructuras de datos, haciéndolos compartir la propiedad de estos recursos del juego. Desafortunadamente, digamos que comete un error cuando olvidó eliminar los elementos del juego de una de las estructuras de datos al avanzar a la siguiente etapa, evitando que se liberen hasta que se cierre todo el juego. Sin embargo, el juego todavía parece funcionar bien porque los elementos no se están dibujando o afectan la interacción del usuario. Sin embargo, el juego está comenzando a usar más y más memoria, mientras que las velocidades de fotogramas funcionan en una presentación de diapositivas, mientras que el procesamiento oculto todavía está recorriendo esta colección oculta de elementos en el juego (que ahora se ha vuelto explosivo en tamaño). Este es el tipo de problema que encuentro con frecuencia en tales juegos Flash.

  • Me he encontrado con personas que dicen que esto no cuenta como una "pérdida de memoria" porque la memoria todavía se está liberando al cerrar la aplicación, y en su lugar podría llamarse una "pérdida de espacio" o algo por el estilo. Si bien tal distinción podría ser útil para identificar y hablar sobre problemas, no encuentro tales distinciones tan útiles en este contexto si hablamos de ello como si no fuera tan problemático como una "pérdida de memoria" cuando estamos tratando El objetivo práctico de garantizar que el software no acumule cantidades ridículas de memoria mientras más lo ejecutamos (a menos que estemos hablando de sistemas operativos oscuros que no liberan la memoria de un proceso cuando finaliza).

Ahora digamos que el mismo desarrollador en ciernes escribió el juego en C ++. En ese caso, normalmente solo habría una estructura de datos central en el juego que "posee" la memoria, mientras que otros apuntan a esa memoria. Si comete el mismo tipo de error, lo más probable es que, al avanzar a la siguiente etapa, el juego se bloquee como resultado de acceder a punteros colgantes (o peor, hacer algo diferente al bloqueo).

Este es el tipo de compensación más inmediato que tiendo a encontrar en mi dominio con mayor frecuencia entre GC y no GC. Y en realidad no me importa mucho GC en mi dominio, lo que no es muy crítico para la misión, porque las mayores dificultades que tuve con el software con fugas involucraron el uso accidental de GC en un antiguo equipo que causó el tipo de fugas descritas anteriormente .

En mi dominio particular, prefiero que el software falle o falle en muchos casos porque es al menos mucho más fácil de detectar que tratar de rastrear por qué el software consume misteriosamente cantidades explosivas de memoria después de ejecutarlo durante media hora mientras todos las pruebas de unidad e integración pasan sin ninguna queja (ni siquiera de Valgrind, ya que la memoria está siendo liberada por GC al apagarse). Sin embargo, eso no es un golpe para GC por mi parte o un intento de decir que es inútil o algo así, pero no ha sido ningún tipo de bala de plata, ni siquiera cercana, en los equipos con los que trabajé contra el software con fugas (para Por el contrario, tuve la experiencia opuesta con esa base de código que utiliza GC siendo la más permeable que he encontrado). Para ser justos, muchos miembros de ese equipo ni siquiera sabían qué referencias débiles eran,

Propiedad compartida y psicología

El problema que encuentro con la recolección de basura que puede hacerlo tan propenso a las "pérdidas de memoria" (e insistiré en llamarlo como tal, la "fuga espacial" se comporta exactamente de la misma manera desde la perspectiva del usuario final) en manos de aquellos que no lo usan con cuidado se relacionan con las "tendencias humanas" hasta cierto punto en mi experiencia. El problema con ese equipo y la base de código con más filtraciones que encontré fue que parecían tener la impresión de que GC les permitiría dejar de pensar en quién posee los recursos.

En nuestro caso, teníamos tantos objetos que se hacían referencia entre sí. Los modelos harían referencia a los materiales junto con la biblioteca de materiales y el sistema de sombreado. Los materiales harían referencia a las texturas junto con la biblioteca de texturas y ciertos sombreadores. Las cámaras almacenarían referencias a todo tipo de entidades de escena que deberían excluirse del renderizado. La lista parecía continuar indefinidamente. Eso hizo que casi cualquier recurso considerable en el sistema fuera propiedad y se extendiera de por vida en más de 10 lugares en el estado de la aplicación a la vez, y eso era muy, muy propenso a errores humanos del tipo que se traduciría en una fuga (y no uno menor, estoy hablando de gigabytes en minutos con serios problemas de usabilidad). Conceptualmente, no era necesario compartir todos estos recursos en propiedad, todos conceptualmente tenían un propietario,

Si dejamos de pensar en quién posee qué memoria, y felizmente solo almacenamos referencias a objetos que se extienden por toda la vida en todo el lugar sin pensar en esto, entonces el software no se bloqueará como resultado de punteros colgantes, pero casi con seguridad, bajo tal mentalidad descuidada, comience a perder memoria como loca en formas que son muy difíciles de rastrear y eludirán las pruebas.

Si hay un beneficio práctico para el puntero colgante en mi dominio, es que causa fallas y fallas muy desagradables. Y eso, al menos, tiende a dar a los desarrolladores el incentivo, si quieren enviar algo confiable, para comenzar a pensar en la gestión de recursos y hacer las cosas adecuadas necesarias para eliminar todas las referencias / punteros adicionales a un objeto que ya no es conceptualmente necesario.

Gestión de recursos de aplicaciones

La gestión adecuada de los recursos es el nombre del juego si estamos hablando de evitar fugas en aplicaciones de larga duración con un estado persistente almacenado donde las fugas plantearían serios problemas de velocidad de fotogramas y usabilidad. Y administrar correctamente los recursos aquí no es menos difícil con o sin GC. El trabajo no es menos manual para eliminar las referencias apropiadas a los objetos que ya no son necesarios, ya sean punteros o referencias que extiendan la vida útil.

Ese es el desafío en mi dominio, sin olvidar deletelo que nosotros new(a menos que estemos hablando de hora de aficionados con pruebas, prácticas y herramientas de mala calidad). Y requiere pensar y cuidar si estamos usando GC o no.

Multithreading

El otro problema que encuentro muy útil con GC, si pudiera usarse con mucha precaución en mi dominio, es simplificar la gestión de recursos en contextos de subprocesos múltiples. Si tenemos cuidado de no almacenar referencias de recursos que se extiendan a lo largo de la vida en más de un lugar en el estado de la aplicación, entonces la naturaleza de las referencias de GC que se extienden a lo largo de la vida podría ser extremadamente útil como una forma para que los hilos extiendan temporalmente un recurso al que se accede para extender es de por vida por solo una corta duración según sea necesario para que el hilo termine de procesarlo.

Creo que un uso muy cuidadoso de GC de esta manera podría generar un software muy correcto que no tenga fugas, al tiempo que simplifica el subprocesamiento múltiple.

Hay formas de evitar esto, aunque ausente GC. En mi caso, unificamos la representación de la entidad de escena del software, con hilos que temporalmente hacen que los recursos de la escena se extiendan por breves períodos de una manera bastante generalizada antes de una fase de limpieza. Esto puede oler un poco a GC pero la diferencia es que no hay una "propiedad compartida" involucrada, solo un diseño de procesamiento de escena uniforme en hilos que difieren la destrucción de dichos recursos. Aún así, sería mucho más simple confiar en GC aquí si pudiera usarse con mucho cuidado con desarrolladores concienzudos, con cuidado de usar referencias débiles en las áreas persistentes relevantes, para tales casos de subprocesos múltiples.

C ++

Finalmente:

En C ++ tengo que llamar a delete para disponer de un objeto creado al final de su ciclo de vida.

En Modern C ++, esto generalmente no es algo que debería hacer manualmente. Ni siquiera se trata de olvidar hacerlo. Cuando involucra el manejo de excepciones en la imagen, incluso si escribió un correspondiente deletedebajo de alguna llamada a new, algo podría lanzarse en el medio y nunca llegar a la deletellamada si no confía en las llamadas destructoras automáticas insertadas por el compilador para hacer esto. tú.

Con C ++ prácticamente necesita, a menos que esté trabajando como un contexto incrustado con excepciones desactivadas y bibliotecas especiales que están programadas deliberadamente para no arrojar, evite dicha limpieza manual de recursos (que incluye evitar llamadas manuales para desbloquear un mutex fuera de un dtor , por ejemplo, y no solo desasignación de memoria). El manejo de excepciones casi lo exige, por lo que toda la limpieza de recursos debe automatizarse a través de destructores en su mayor parte.

Dragon Energy
fuente