¿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/malloc
sin un correspondiente a delete/free
menudo 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/freeing
en 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 delete
lo 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 delete
debajo de alguna llamada a new
, algo podría lanzarse en el medio y nunca llegar a la delete
llamada 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.