Fuites physiques
Le genre de bogues que GC corrige semble (au moins pour un observateur externe) le genre de choses qu'un programmeur qui connaît bien son langage, ses bibliothèques, ses concepts, ses idiomes, etc., ne ferait pas. Mais je peux me tromper: la gestion manuelle de la mémoire est-elle intrinsèquement compliquée?
Venant de l'extrémité C qui rend la gestion de la mémoire aussi manuelle et prononcée que possible afin que nous comparions les extrêmes (C ++ automatise principalement la gestion de la mémoire sans GC), je dirais "pas vraiment" dans le sens de la comparaison avec GC quand il vient à des fuites . Un débutant et parfois même un pro peut oublier d'écrire free
pour une donnée malloc
. Cela arrive vraiment.
Cependant, il existe des outils comme valgrind
la détection des fuites qui repèrent immédiatement, lors de l'exécution du code, quand / où de telles erreurs se produisent jusqu'à la ligne de code exacte. Lorsque cela est intégré dans le CI, il devient presque impossible de fusionner de telles erreurs, et facile comme bonjour pour les corriger. Ce n'est donc jamais un gros problème dans une équipe / un processus avec des normes raisonnables.
Certes, il peut y avoir des cas d'exécution exotiques qui passent sous le radar des tests où ils free
n'ont pas été appelés, peut-être en rencontrant une erreur d'entrée externe obscure comme un fichier corrompu, auquel cas le système peut perdre 32 octets ou quelque chose. Je pense que cela peut certainement se produire même avec de très bonnes normes de test et des outils de détection des fuites, mais il ne serait pas tout aussi critique de laisser un peu de mémoire sur quelque chose qui ne se produit presque jamais. Nous verrons un problème beaucoup plus important où nous pouvons divulguer des ressources massives même dans les chemins d'exécution courants ci-dessous d'une manière que le GC ne peut pas empêcher.
C'est aussi difficile sans quelque chose qui ressemble à une pseudo-forme de GC (comptage de références, par exemple) lorsque la durée de vie d'un objet doit être prolongée pour une certaine forme de traitement différé / asynchrone, peut-être par un autre thread.
Pointeurs pendants
Le vrai problème avec des formes plus manuelles de gestion de la mémoire n'est pas une fuite pour moi. Combien d'applications natives écrites en C ou C ++ connaissons-nous vraiment qui fuient? Le noyau Linux fuit-il? MySQL? CryEngine 3? Stations de travail et synthétiseurs audio numériques? Est-ce que Java VM fuit (il est implémenté en code natif)? Photoshop?
Si quoi que ce soit, je pense que lorsque nous regardons autour de nous, les applications qui fuient ont tendance à être celles écrites à l'aide de schémas GC. Mais avant que cela ne soit considéré comme un slam sur la récupération de place, le code natif a un problème important qui n'est pas du tout lié aux fuites de mémoire.
Le problème pour moi était toujours la sécurité. Même lorsque nous free
mémorisons un pointeur, s'il existe d'autres pointeurs vers la ressource, ils deviendront des pointeurs pendants (invalidés).
Lorsque nous essayons d'accéder aux pointes de ces pointeurs pendants, nous finissons par avoir un comportement indéfini, bien que presque toujours une faute de segmentation / violation d'accès conduisant à un crash dur et immédiat.
Toutes ces applications natives que j'ai énumérées ci-dessus ont potentiellement un ou deux cas de bord obscurs qui peuvent conduire à un crash principalement à cause de ce problème, et il y a certainement une bonne part d'applications de mauvaise qualité écrites en code natif qui sont très lourdes, et souvent en grande partie à cause de ce problème.
... et c'est parce que la gestion des ressources est difficile, que vous utilisiez GC ou non. La différence pratique est souvent une fuite (GC) ou un crash (sans GC) face à une erreur entraînant une mauvaise gestion des ressources.
Gestion des ressources: garbage collection
La gestion complexe des ressources est un processus manuel difficile, quoi qu'il arrive. GC ne peut rien automatiser ici.
Prenons un exemple où nous avons cet objet, "Joe". Joe est référencé par un certain nombre d'organisations dont il est membre. Chaque mois environ, ils extraient une cotisation de sa carte de crédit.
Nous avons également une référence à Joe pour contrôler sa vie. Disons qu'en tant que programmeurs, nous n'avons plus besoin de Joe. Il commence à nous harceler et nous n'avons plus besoin de ces organisations auxquelles il appartient pour perdre leur temps à traiter avec lui. Nous essayons donc de l'essuyer de la surface de la terre en supprimant sa référence de ligne de vie.
... mais attendez, nous utilisons la collecte des ordures. Chaque référence forte à Joe le maintiendra. Nous supprimons donc également les références à lui des organisations auxquelles il appartient (en le désinscrivant).
... sauf whoops, nous avons oublié d'annuler son abonnement au magazine! Maintenant, Joe reste dans la mémoire, nous harcèle et utilise des ressources, et la société de magazines continue également de traiter l'adhésion de Joe chaque mois.
C'est la principale erreur qui peut entraîner la fuite de nombreux programmes complexes écrits à l'aide de schémas de collecte de déchets et commencer à utiliser de plus en plus de mémoire au fur et à mesure qu'ils s'exécutent, et éventuellement de plus en plus de traitement (l'abonnement récurrent au magazine). Ils ont oublié de supprimer une ou plusieurs de ces références, ce qui empêche le garbage collector de faire sa magie jusqu'à ce que le programme entier soit arrêté.
Cependant, le programme ne plante pas. C'est parfaitement sûr. Ça va juste continuer à accumuler de la mémoire et Joe s'attardera toujours. Pour de nombreuses applications, ce type de comportement qui fuit où nous jetons de plus en plus de mémoire / traitement sur le problème pourrait être de loin préférable à un crash dur, surtout compte tenu de la quantité de mémoire et de puissance de traitement de nos machines aujourd'hui.
Gestion des ressources: manuel
Considérons maintenant l'alternative où nous utilisons des pointeurs vers Joe et la gestion manuelle de la mémoire, comme ceci:
Ces liens bleus ne gèrent pas la vie de Joe. Si nous voulons le retirer de la surface de la terre, nous demandons manuellement de le détruire, comme ceci:
Maintenant, cela nous laisserait normalement des pointeurs pendants partout, alors retirons les pointeurs à Joe.
... oups, nous avons encore fait exactement la même erreur et oublié de se désabonner de l'abonnement au magazine Joe!
Sauf que maintenant, nous avons un pointeur pendant. Lorsque l'abonnement au magazine essaie de traiter les frais mensuels de Joe, le monde entier explose - généralement, nous obtenons le crash dur instantanément.
Cette même erreur de base de mauvaise gestion des ressources où le développeur a oublié de supprimer manuellement tous les pointeurs / références à une ressource peut entraîner de nombreux plantages dans les applications natives. Ils ne monopolisent pas de mémoire plus ils courent généralement, car ils se planteront souvent carrément dans ce cas.
Monde réel
Maintenant, l'exemple ci-dessus utilise un diagramme ridiculement simple. Une application réelle peut nécessiter des milliers d'images assemblées pour couvrir un graphique complet, avec des centaines de types de ressources différents stockés dans un graphique de scène, des ressources GPU associées à certaines d'entre elles, des accélérateurs liés à d'autres, des observateurs répartis sur des centaines de plugins regarder un certain nombre de types d'entités dans la scène pour les changements, les observateurs observant les observateurs, les audios synchronisés avec les animations, etc. Il peut donc sembler facile d'éviter l'erreur que j'ai décrite ci-dessus, mais ce n'est généralement pas du tout aussi simple dans un monde réel base de code de production pour une application complexe couvrant des millions de lignes de code.
La chance que quelqu'un, un jour, mal gère les ressources quelque part dans cette base de code a tendance à être assez élevée, et cette probabilité est la même avec ou sans GC. La principale différence est ce qui se passera à la suite de cette erreur, ce qui affecte également potentiellement la vitesse à laquelle cette erreur sera détectée et corrigée.
Crash vs Leak
Maintenant, lequel est le pire? Un crash immédiat ou une fuite de mémoire silencieuse où Joe s'attarde mystérieusement?
La plupart pourraient répondre à cette dernière, mais disons que ce logiciel est conçu pour fonctionner pendant des heures, voire des jours, et chacun de ces Joe et Jane que nous ajoutons augmente l'utilisation de la mémoire du logiciel d'un gigaoctet. Ce n'est pas un logiciel critique (les plantages ne tuent pas réellement les utilisateurs), mais critique en termes de performances.
Dans ce cas, un crash dur qui apparaît immédiatement lors du débogage, soulignant l'erreur que vous avez faite, pourrait en fait être préférable à un logiciel qui fuit et qui pourrait même passer sous le radar de votre procédure de test.
D'un autre côté, s'il s'agit d'un logiciel essentiel à la mission où les performances ne sont pas le but, tout simplement ne pas planter par tous les moyens possibles, une fuite pourrait en fait être préférable.
Références faibles
Il existe une sorte d'hybride de ces idées disponibles dans les schémas GC appelés références faibles. Avec des références faibles, nous pouvons avoir toutes ces organisations référence faible Joe mais ne pas l'empêcher d'être supprimé lorsque la référence forte (propriétaire / ligne de vie de Joe) disparaît. Néanmoins, nous avons l'avantage de pouvoir détecter quand Joe n'est plus là grâce à ces références faibles, ce qui nous permet d'obtenir une sorte d'erreur facilement reproductible.
Malheureusement, les références faibles ne sont pas utilisées presque autant qu'elles devraient probablement être utilisées, donc souvent de nombreuses applications GC complexes peuvent être sensibles aux fuites même si elles sont potentiellement beaucoup moins planteuses qu'une application C complexe, par exemple
Dans tous les cas, le fait que GC vous rende la vie plus facile ou plus difficile dépend de l'importance pour votre logiciel d'éviter les fuites, et qu'il traite ou non d'une gestion complexe des ressources de ce type.
Dans mon cas, je travaille dans un domaine critique pour les performances où les ressources s'étendent sur des centaines de mégaoctets à gigaoctets, et ne pas libérer cette mémoire lorsque les utilisateurs demandent à décharger à cause d'une erreur comme la précédente peut en fait être moins préférable à un crash. Les plantages sont faciles à repérer et à reproduire, ce qui en fait souvent le type de bogue préféré du programmeur, même s'il est le moins préféré de l'utilisateur, et beaucoup de ces plantages apparaîtront avec une procédure de test sensée avant même d'atteindre l'utilisateur.
Quoi qu'il en soit, ce sont les différences entre GC et la gestion manuelle de la mémoire. Pour répondre à votre question immédiate, je dirais que la gestion manuelle de la mémoire est difficile, mais elle a très peu à voir avec les fuites, et les formes de gestion de la mémoire GC et manuelles sont toujours très difficiles lorsque la gestion des ressources n'est pas triviale. Le GC a sans doute un comportement plus délicat ici où le programme semble fonctionner très bien mais consomme de plus en plus de ressources. Le formulaire manuel est moins compliqué, mais va planter et brûler beaucoup de temps avec des erreurs comme celle illustrée ci-dessus.