Fugas físicas
El tipo de errores que aborda GC parece (al menos para un observador externo) el tipo de cosas que un programador que conoce bien su lenguaje, bibliotecas, conceptos, modismos, etc., no haría. Pero podría estar equivocado: ¿el manejo manual de la memoria es intrínsecamente complicado?
Viniendo del extremo C que hace que la gestión de la memoria sea lo más manual y pronunciada posible para que podamos comparar los extremos (C ++ automatiza principalmente la gestión de la memoria sin GC), diría "no realmente" en el sentido de comparar con GC cuando llega a las fugas . Un principiante y, a veces, incluso un profesional pueden olvidarse de escribir free
para un determinado malloc
. Definitivamente sucede.
Sin embargo, existen herramientas como la valgrind
detección de fugas que detectarán inmediatamente, al ejecutar el código, cuándo / dónde se producen dichos errores hasta la línea exacta de código. Cuando está integrado en el CI, se vuelve casi imposible fusionar tales errores, y es fácil como corregirlos. Por lo tanto, nunca es un gran problema en ningún equipo / proceso con estándares razonables.
De acuerdo, podría haber algunos casos exóticos de ejecución que pasan desapercibidos cuando free
no se llamaron, tal vez al encontrar un oscuro error de entrada externo como un archivo corrupto, en cuyo caso el sistema puede perder 32 bytes o algo así. Creo que eso definitivamente puede suceder incluso bajo estándares de prueba bastante buenos y herramientas de detección de fugas, pero tampoco sería tan crítico perder un poco de memoria en algo que casi nunca sucede. Veremos un problema mucho más grande en el que podemos filtrar recursos masivos incluso en las rutas de ejecución comunes a continuación de una manera que GC no puede evitar.
También es difícil sin algo parecido a una pseudoforma de GC (recuento de referencias, por ejemplo) cuando la vida útil de un objeto debe extenderse para alguna forma de procesamiento diferido / asincrónico, tal vez por otro hilo.
Punteros colgantes
El problema real con más formas manuales de administración de memoria no es para mí las filtraciones. ¿Cuántas aplicaciones nativas escritas en C o C ++ sabemos que realmente tienen fugas? ¿El kernel de Linux tiene fugas? MySQL? CryEngine 3? ¿Estaciones de trabajo de audio digital y sintetizadores? ¿Fuga Java VM (está implementado en código nativo)? Photoshop?
En todo caso, creo que cuando miramos a nuestro alrededor, las aplicaciones con más fugas tienden a ser las escritas usando esquemas GC. Pero antes de que se tome como un golpe en la recolección de basura, el código nativo tiene un problema importante que no está relacionado en absoluto con las pérdidas de memoria.
El problema para mí siempre fue la seguridad. Incluso cuando recordamos a free
través de un puntero, si hay otros punteros al recurso, se convertirán en punteros colgantes (invalidados).
Cuando intentamos acceder a los punteros de esos punteros colgantes, terminamos con un comportamiento indefinido, aunque casi siempre una violación de seguridad / acceso que conduce a un bloqueo duro e inmediato.
Todas esas aplicaciones nativas que mencioné anteriormente tienen potencialmente un caso oscuro o dos que pueden provocar un bloqueo principalmente debido a este problema, y definitivamente hay una buena cantidad de aplicaciones de mala calidad escritas en código nativo que son muy pesadas, y a menudo en gran parte debido a este problema.
... y es porque la administración de recursos es difícil independientemente de si usa GC o no. La diferencia práctica es a menudo fugas (GC) o fallas (sin GC) ante un error que conduce a una mala gestión de los recursos.
Gestión de recursos: recolección de basura
La gestión compleja de recursos es un proceso manual difícil, pase lo que pase. GC no puede automatizar nada aquí.
Tomemos un ejemplo donde tenemos este objeto, "Joe". Joe es referenciado por varias organizaciones a las que es miembro. Cada mes aproximadamente extraen una cuota de membresía de su tarjeta de crédito.
También tenemos una referencia a Joe para controlar su vida. Digamos que, como programadores, ya no necesitamos a Joe. Está empezando a molestarnos y ya no necesitamos que estas organizaciones a las que pertenece desperdicien su tiempo tratando con él. Así que intentamos borrarlo de la faz de la tierra eliminando su referencia de línea de vida.
... pero espera, estamos usando recolección de basura. Cada referencia fuerte a Joe lo mantendrá cerca. Por lo tanto, también eliminamos las referencias a él de las organizaciones a las que pertenece (cancelando su suscripción).
... excepto los gritos, ¡olvidamos cancelar su suscripción a la revista! Ahora Joe permanece en la memoria, molestándonos y usando recursos, y la compañía de revistas también termina procesando la membresía de Joe todos los meses.
Este es el error principal que puede causar que muchos programas complejos escritos usando esquemas de recolección de basura se filtren y comiencen a usar más y más memoria cuanto más tiempo se ejecuten, y posiblemente más y más procesamiento (la suscripción recurrente a la revista). Se olvidaron de eliminar una o más de esas referencias, haciendo imposible que el recolector de basura haga su magia hasta que se cierre todo el programa.
Sin embargo, el programa no se bloquea. Es perfectamente seguro Simplemente va a seguir acumulando memoria y Joe todavía se quedará por ahí. Para muchas aplicaciones, este tipo de comportamiento con fugas en el que simplemente arrojamos más y más memoria / procesamiento al problema podría ser mucho más preferible que un bloqueo duro, especialmente dada la cantidad de memoria y potencia de procesamiento que nuestras máquinas tienen hoy en día.
Gestión de recursos: manual
Ahora consideremos la alternativa en la que usamos punteros a Joe y la administración manual de memoria, así:
Estos enlaces azules no gestionan la vida de Joe. Si queremos eliminarlo de la faz de la tierra, solicitamos manualmente destruirlo, así:
Ahora eso normalmente nos dejaría con punteros colgantes por todo el lugar, así que vamos a quitarle los punteros a Joe.
... ¡Vaya, cometimos exactamente el mismo error nuevamente y olvidamos cancelar la suscripción a la revista de Joe!
Excepto que ahora tenemos un puntero colgante. Cuando la suscripción a la revista intente procesar la tarifa mensual de Joe, todo el mundo explotará, por lo general, tenemos un colapso duro al instante.
Este mismo error básico de mala administración de recursos en el que el desarrollador olvidó eliminar manualmente todos los punteros / referencias a un recurso puede provocar muchos bloqueos en las aplicaciones nativas. Por lo general, no acumulan memoria mientras más tiempo se ejecutan, ya que a menudo se bloquean en este caso.
Mundo real
Ahora el ejemplo anterior está usando un diagrama ridículamente simple. Una aplicación del mundo real puede requerir miles de imágenes unidas para cubrir un gráfico completo, con cientos de diferentes tipos de recursos almacenados en un gráfico de escena, recursos de GPU asociados a algunos de ellos, aceleradores vinculados a otros, observadores distribuidos en cientos de complementos observando una serie de tipos de entidades en la escena en busca de cambios, observadores observadores observadores, audios sincronizados con animaciones, etc. Por lo tanto, puede parecer que es fácil evitar el error que describí anteriormente, pero generalmente no es tan simple en un mundo real base de código de producción para una aplicación compleja que abarca millones de líneas de código.
La posibilidad de que alguien, algún día, administre mal los recursos en algún lugar de esa base de código tiende a ser bastante alta, y esa probabilidad es la misma con o sin GC. La principal diferencia es lo que sucederá como resultado de este error, que también afecta potencialmente la rapidez con la que este error se detectará y solucionará.
Choque contra fugas
¿Ahora cuál es peor? ¿Un choque inmediato o una fuga silenciosa de memoria donde Joe se queda misteriosamente?
La mayoría podría responder a esto último, pero digamos que este software está diseñado para ejecutarse durante horas, posiblemente días, y cada uno de estos Joe's y Jane's que agregamos aumenta el uso de memoria del software en un gigabyte. No es un software de misión crítica (los bloqueos en realidad no matan a los usuarios), sino uno de rendimiento crítico.
En este caso, un bloqueo duro que aparece inmediatamente al depurar, señalando el error que cometió, podría ser preferible a un software con fugas que incluso podría pasar desapercibido en su procedimiento de prueba.
Por otro lado, si se trata de un software de misión crítica donde el rendimiento no es el objetivo, simplemente no se bloquea de ninguna manera posible, entonces la filtración podría ser preferible.
Referencias débiles
Hay una especie de híbrido de estas ideas disponible en los esquemas de GC conocidos como referencias débiles. Con referencias débiles, podemos hacer que todas estas organizaciones hagan referencia débil a Joe, pero no evitar que sea eliminado cuando la referencia fuerte (propietario / línea de vida de Joe) desaparece. Sin embargo, obtenemos el beneficio de poder detectar cuándo Joe ya no está cerca a través de estas referencias débiles, lo que nos permite obtener un tipo de error fácilmente reproducible.
Desafortunadamente, las referencias débiles no se usan tanto como probablemente deberían usarse, por lo que muchas aplicaciones complejas de GC pueden ser susceptibles a fugas, incluso si son potencialmente menos defectuosas que una aplicación C compleja, por ejemplo
En cualquier caso, si GC hace o no su vida más fácil o más difícil depende de lo importante que sea para su software evitar fugas, y de si se trata o no de una gestión de recursos compleja de este tipo.
En mi caso, trabajo en un campo de rendimiento crítico donde los recursos abarcan cientos de megabytes a gigabytes, y no libero esa memoria cuando los usuarios solicitan la descarga debido a un error como el anterior en realidad puede ser menos preferible a un bloqueo. Los bloqueos son fáciles de detectar y reproducir, lo que los convierte a menudo en el tipo de error favorito del programador, incluso si es el menos favorito del usuario, y muchos de estos bloqueos aparecerán con un procedimiento de prueba sensato incluso antes de que lleguen al usuario.
De todos modos, esas son las diferencias entre GC y la gestión de memoria manual. Para responder a su pregunta inmediata, diría que la administración manual de la memoria es difícil, pero tiene muy poco que ver con las fugas, y tanto la GC como las formas manuales de administración de la memoria siguen siendo muy difíciles cuando la administración de recursos no es trivial. Podría decirse que el GC tiene un comportamiento más complicado aquí donde el programa parece estar funcionando bien pero está consumiendo más y más y más recursos. El formulario manual es menos complicado, pero se bloqueará y quemará a lo grande con errores como el que se muestra arriba.