Quelles sont les complexités de la programmation non gérée en mémoire?


24

Ou en d'autres termes, quels problèmes spécifiques la collecte automatisée des déchets a-t-elle résolus? Je n'ai jamais fait de programmation de bas niveau, donc je ne sais pas à quel point la libération des ressources peut être compliquée.

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?


3
Veuillez développer pour nous dire comment votre question ne reçoit pas de réponse dans l'article de Wikipédia sur la collection de vêtements et plus spécifiquement dans la section sur ses avantages
yannis

Un autre avantage est la sécurité, par exemple les dépassements de tampon sont hautement exploitables et de nombreuses autres vulnérabilités de sécurité proviennent de la mauvaise gestion de la mémoire.
StuperUser

7
@StuperUser: Cela n'a rien à voir avec l'origine de la mémoire. Vous pouvez très bien tamponner la mémoire de dépassement provenant d'un GC. Le fait que les langages GC empêchent généralement cela est orthogonal, et les langages qui ont moins de trente ans de retard sur la technologie GC que vous les comparez pour offrir également une protection contre le dépassement de tampon.
DeadMG

Réponses:


29

Je n'ai jamais fait de programmation de bas niveau, donc je ne sais pas à quel point la libération des ressources peut être compliquée.

C'est drôle comme la définition de "bas niveau" change avec le temps. Lorsque j'ai appris à programmer pour la première fois, tout langage qui fournissait un modèle de segment standardisé qui rend possible un modèle d'allocation / libération simple était en effet considéré comme de haut niveau. Dans la programmation de bas niveau , vous devez garder une trace de la mémoire vous-même (pas les allocations, mais les emplacements de mémoire eux-mêmes!), Ou écrire votre propre allocateur de tas si vous vous sentez vraiment fantaisiste.

Cela dit, il n'y a vraiment rien d'effrayant ni de "compliqué" à ce sujet. Rappelez-vous quand vous étiez enfant et que votre maman vous a dit de ranger vos jouets lorsque vous avez fini de jouer avec eux, qu'elle n'était pas votre femme de chambre et n'allait pas nettoyer votre chambre pour vous? La gestion de la mémoire est simplement le même principe appliqué au code. (GC est comme avoir une femme de chambre qui va nettoyer après vous, mais elle est très paresseux et un peu désemparés.) Le principe de c'est simple: Chaque variable dans votre code a un et un seul propriétaire, et il est de la responsabilité de ce propriétaire libérer la mémoire de la variable lorsqu'elle n'est plus nécessaire. ( Le principe de propriété unique) Cela nécessite un appel par allocation, et plusieurs schémas existent qui automatisent la propriété et le nettoyage d'une manière ou d'une autre afin que vous n'ayez même pas à écrire cet appel dans votre propre code.

La collecte des ordures est censée résoudre deux problèmes. Il fait invariablement un très mauvais travail sur l'un d'entre eux et, selon la mise en œuvre, peut ou non bien fonctionner avec l'autre. Les problèmes sont les fuites de mémoire (conserver la mémoire une fois que vous en avez terminé) et les références pendantes (libérer de la mémoire avant d'en avoir terminé). Examinons les deux problèmes:

Références pendantes: Discutez d'abord de celle-ci parce que c'est vraiment la plus sérieuse. Vous avez deux pointeurs vers le même objet. Vous en libérez un et vous ne remarquez pas l'autre. Ensuite, à un moment ultérieur, vous essayez de lire (ou d'écrire ou de libérer) le second. Un comportement indéfini s'ensuit. Si vous ne le remarquez pas, vous pouvez facilement corrompre votre mémoire. La récupération de place est censée rendre ce problème impossible en garantissant que rien ne sera jamais libéré tant que toutes les références à celui-ci n'auront pas disparu. Dans un langage entièrement géré, cela fonctionne presque jusqu'à ce que vous ayez à gérer des ressources mémoire externes non gérées. Ensuite, c'est de retour à la case 1. Et dans un langage non géré, les choses sont encore plus difficiles. (Poke around sur Mozilla '

Heureusement, traiter ce problème est fondamentalement un problème résolu. Vous n'avez pas besoin d'un garbage collector, vous avez besoin d'un gestionnaire de mémoire de débogage. J'utilise Delphi, par exemple, et avec une seule bibliothèque externe et une simple directive de compilation, je peux définir l'allocateur en "Mode de débogage complet". Cela ajoute un surcoût de performances négligeable (moins de 5%) en contrepartie de l'activation de certaines fonctionnalités qui gardent une trace de la mémoire utilisée. Si je libère un objet, il remplit sa mémoire de0x80octets (facilement reconnaissable dans le débogueur) et si j'essaie d'appeler une méthode virtuelle (y compris le destructeur) sur un objet libéré, il remarque et interrompt le programme avec une boîte d'erreur avec trois traces de pile - lorsque l'objet a été créé, quand il a été libéré, et où je suis maintenant - plus quelques autres informations utiles, soulève alors une exception. Ceci n'est évidemment pas adapté aux versions, mais cela rend la recherche et la résolution des problèmes de référence pendantes triviales.

Le deuxième problème concerne les fuites de mémoire. C'est ce qui se produit lorsque vous continuez à conserver la mémoire allouée lorsque vous n'en avez plus besoin. Cela peut arriver dans n'importe quelle langue, avec ou sans garbage collection, et ne peut être corrigé qu'en écrivant votre code correctement. Le garbage collection permet d'atténuer une forme spécifique de fuite de mémoire, du genre de celle qui se produit lorsque vous n'avez aucune référence valide à un morceau de mémoire qui n'a pas encore été libéré, ce qui signifie que la mémoire reste allouée jusqu'à la fin du programme. Malheureusement, la seule façon d'accomplir cela de manière automatisée est de transformer chaque allocation en une fuite de mémoire!

Je vais probablement me laisser impressionner par les partisans du GC si j'essaie de dire quelque chose comme ça, alors permettez-moi de vous expliquer. Rappelez-vous que la définition d'une fuite de mémoire se maintient sur la mémoire allouée lorsque vous n'en avez plus besoin. En plus de n'avoir aucune référence à quelque chose, vous pouvez également fuir la mémoire en y ayant une référence inutile, comme la conserver dans un objet conteneur alors que vous auriez dû la libérer. J'ai vu des fuites de mémoire causées par cela, et il est très difficile de savoir si vous avez un GC ou non, car elles impliquent une référence parfaitement valide à la mémoire et il n'y a pas de "bogues" clairs pour les outils de débogage pour capture. Pour autant que je sache, il n'y a pas d'outil automatisé qui vous permet d'attraper ce type de fuite de mémoire.

Ainsi, un garbage collector ne se préoccupe que de la variété sans référence des fuites de mémoire, car c'est le seul type qui peut être traité de manière automatisée. S'il pouvait regarder toutes vos références à tout et libérer chaque objet dès qu'il a zéro référence pointant vers lui, ce serait parfait, au moins en ce qui concerne le problème des non-références. Faire cela de manière automatisée s'appelle le comptage de références, et cela peut être fait dans certaines situations limitées, mais il a ses propres problèmes à traiter. (Par exemple, l'objet A contenant une référence à l'objet B, qui contient une référence à l'objet A. Dans un schéma de comptage de références, aucun objet ne peut être libéré automatiquement, même en l'absence de références externes à A ou à B.) Donc les éboueurs utilisent le traçageà la place: commencez par un ensemble d'objets connus, recherchez tous les objets qu'ils référencent, trouvez tous les objets qu'ils référencent, et ainsi de suite récursivement jusqu'à ce que vous ayez tout trouvé. Tout ce qui ne se trouve pas dans le processus de traçage est un déchet et peut être jeté. (Pour ce faire, il faut bien sûr un langage géré qui impose certaines restrictions au système de type pour garantir que le ramasse-miettes de traçage puisse toujours faire la différence entre une référence et une mémoire aléatoire qui ressemble à un pointeur.)

Il y a deux problèmes avec le traçage. Tout d'abord, c'est lent, et pendant ce temps, le programme doit être plus ou moins interrompu pour éviter les conditions de course. Cela peut entraîner des hoquets d'exécution notables lorsque le programme est censé interagir avec un utilisateur, ou des performances en panne dans une application serveur. Cela peut être atténué par diverses techniques, telles que la décomposition de la mémoire allouée en "générations" sur le principe que si une allocation n'est pas collectée la première fois que vous essayez, elle est susceptible de rester un moment. Le framework .NET et la JVM utilisent tous les deux des récupérateurs de place générationnels.

Malheureusement, cela alimente le deuxième problème: la mémoire n'est pas libérée lorsque vous en avez terminé. À moins que le traçage ne soit exécuté immédiatement après que vous ayez fini avec un objet, il restera jusqu'à la prochaine trace, ou même plus longtemps s'il dépasse la première génération. En fait, l' une des meilleures explications du ramasse-miettes .NET que j'ai vues explique que, pour accélérer le processus le plus rapidement possible, le GC doit reporter la collecte aussi longtemps qu'il le peut! Ainsi, le problème des fuites de mémoire est "résolu" assez bizarrement en laissant couler autant de mémoire que possible aussi longtemps que possible! C'est ce que je veux dire quand je dis qu'un GC transforme chaque allocation en une fuite de mémoire. En fait, il n'y a aucune garantie qu'un objet donné sera jamais collecté.

Pourquoi est-ce un problème, alors que la mémoire est toujours récupérée en cas de besoin? Pour plusieurs raisons. Imaginez d'abord que vous allouiez un grand objet (un bitmap, par exemple) qui prend une quantité importante de mémoire. Et peu de temps après, vous avez besoin d'un autre gros objet qui occupe la même (ou presque la même) quantité de mémoire. Si le premier objet avait été libéré, le second pourrait réutiliser sa mémoire. Mais sur un système récupéré, vous attendez peut-être toujours que la prochaine trace s'exécute, et vous finissez par perdre inutilement de la mémoire pour un deuxième grand objet. C'est fondamentalement une condition de course.

Deuxièmement, conserver la mémoire inutilement, en particulier en grande quantité, peut entraîner des problèmes dans un système multitâche moderne. Si vous occupez trop de mémoire physique, cela peut obliger votre programme ou d'autres programmes à paginer (échanger une partie de leur mémoire sur disque), ce qui ralentit vraiment les choses. Pour certains systèmes, tels que les serveurs, la pagination peut non seulement ralentir le système, mais elle peut tout bloquer si elle est sous charge.

Comme le problème des références pendantes, le problème sans référence peut être résolu avec un gestionnaire de mémoire de débogage. Encore une fois, je mentionnerai le mode de débogage complet du gestionnaire de mémoire FastMM de Delphi, car c'est celui que je connais le mieux. (Je suis sûr que des systèmes similaires existent pour d'autres langues.)

Lorsqu'un programme exécuté sous FastMM se termine, vous pouvez éventuellement lui signaler l'existence de toutes les allocations qui n'ont jamais été libérées. Le mode de débogage complet va un peu plus loin: il peut enregistrer un fichier sur disque contenant non seulement le type d'allocation, mais une trace de pile depuis son allocation et d'autres informations de débogage, pour chaque allocation perdue. Cela rend la recherche de fuites de mémoire sans référence triviale.

Lorsque vous l'examinez vraiment, la récupération de place peut ou non être efficace pour empêcher les références pendantes, et fait généralement un mauvais travail pour gérer les fuites de mémoire. Sa seule vertu, en fait, n'est pas le ramassage des ordures lui-même, mais un effet secondaire: il fournit un moyen automatisé d'effectuer le compactage de tas. Cela peut éviter un problème obscur (épuisement de la mémoire par fragmentation de tas) qui peut tuer des programmes qui s'exécutent continuellement pendant une longue période et ont un taux élevé de désabonnement de la mémoire, et le compactage de tas est à peu près impossible sans garbage collection. Cependant, tout bon allocateur de mémoire utilise de nos jours des compartiments pour minimiser la fragmentation, ce qui signifie que la fragmentation ne devient vraiment un problème que dans des circonstances extrêmes. Pour un programme dans lequel la fragmentation de tas est susceptible d'être un problème, il ' s conseillé d'utiliser un ramasse-miettes compact. Mais l'OMI dans tous les autres cas, l'utilisation de la collecte des ordures est une optimisation prématurée, et de meilleures solutions existent aux problèmes qu'elle "résout".


5
J'adore cette réponse - je la lis de temps en temps. Je ne peux pas faire de remarque pertinente, donc tout ce que je peux dire c'est - merci.
vemv

3
Je voudrais souligner que oui, les GC ont tendance à "fuir" la mémoire (au moins pendant un certain temps), mais ce n'est pas un problème car il collectera la mémoire lorsque l'allocateur de mémoire ne peut pas allouer de mémoire avant la collecte. Avec un langage non GC, une fuite reste toujours une fuite, ce qui signifie que vous pouvez réellement manquer de mémoire en raison d'une mémoire trop importante non collectée. "la collecte des ordures est une optimisation prématurée" ... GC n'est pas une optimisation et n'a pas été conçu dans cet esprit. Sinon, bonne réponse.
Thomas Eding

7
@ThomasEding: GC est certainement une optimisation; il optimise pour l' effort minimal du programmeur, au détriment des performances et de diverses autres mesures de qualité du programme.
Mason Wheeler

5
C'est drôle que vous pointez le traqueur de bogues de Mozilla à un moment donné, parce que Mozilla est arrivé à une conclusion très différente. Firefox avait et continue d'avoir d'innombrables problèmes de sécurité dus à des erreurs de gestion de la mémoire. Notez qu'il ne s'agit pas de savoir comment il était facile de corriger l'erreur une fois détectée --- généralement, les dommages sont déjà causés au moment où les développeurs prennent conscience du problème. Mozilla finance précisément le langage de programmation Rust pour aider à empêcher de telles erreurs d'être introduites en premier lieu.

1
Rust n'utilise cependant pas la collecte des ordures, il utilise le comptage des références exactement comme décrit Mason, juste avec des vérifications approfondies au moment de la compilation plutôt que d'avoir à utiliser un débogueur pour détecter les erreurs au moment de l'exécution ...
Sean Burton

13

Envisager une technique de gestion de la mémoire non récupérée d'une époque équivalente comme les garbage collector utilisés dans les systèmes populaires actuels, tels que le RAII de C ++. Compte tenu de cette approche, le coût de la non-utilisation de la récupération de place automatisée est minime, et GC présente de nombreux problèmes propres. En tant que tel, je dirais que «pas beaucoup» est la réponse à votre problème.

Rappelez-vous, quand les gens pensent à non-GC, ils pensent mallocet free. Mais c'est une erreur logique géante - vous compareriez la gestion des ressources non GC du début des années 1970 aux récupérateurs de la fin des années 90. Ceci est évidemment un comparison- plutôt injuste les éboueurs qui étaient utilisés quand mallocet freeont été conçus étaient beaucoup trop lent pour exécuter un programme significatif, si je me souviens bien. Comparer quelque chose d'une période vaguement équivalente, par exemple unique_ptr, est beaucoup plus significatif.

Les ramasseurs de déchets peuvent gérer les cycles de référence plus facilement, bien que ce soient des expériences assez rares. De plus, les GC peuvent simplement "lancer" du code car le GC se chargera de toute la gestion de la mémoire, ce qui signifie qu'ils peuvent conduire à des cycles de développement plus rapides.

D'un autre côté, ils ont tendance à rencontrer d'énormes problèmes lorsqu'ils traitent avec de la mémoire provenant de n'importe où, à l'exception de leur propre pool GC. De plus, ils perdent beaucoup de leurs avantages lorsque la concurrence est impliquée, car vous devez quand même considérer la propriété de l'objet.

Edit: Beaucoup des choses que vous mentionnez n'ont rien à voir avec GC. Vous confondez la gestion de la mémoire et l'orientation des objets. Voyez, voici le problème: si vous programmez dans un système non géré complet, comme C ++, vous pouvez vérifier autant de limites que vous le souhaitez, et les classes de conteneur standard le proposent. Il n'y a rien de GC à propos de la vérification des limites, par exemple, ou de la frappe forte.

Les problèmes que vous mentionnez sont résolus par l'orientation des objets, pas par GC. L'origine de la mémoire du tableau et le fait de ne pas écrire à l'extérieur sont des concepts orthogonaux.

Edit: Il convient de noter que des techniques plus avancées peuvent éviter la nécessité de toute forme d'allocation dynamique de mémoire. Par exemple, envisagez l'utilisation de this , qui implémente la combinaison Y en C ++ sans aucune allocation dynamique.


La discussion étendue ici a été nettoyée: si tout le monde peut prendre la parole pour discuter du sujet plus avant, je l'apprécierais vraiment.

@DeadMG, savez-vous quel combinateur est censé faire? Il est censé COMBINER. Par définition, le combinateur est une fonction sans aucune variable libre.
SK-logic

2
@ SK-logic: J'aurais pu choisir de l'implémenter uniquement par modèle et de ne pas avoir de variables membres. Mais alors vous ne seriez pas en mesure de passer dans les fermetures, ce qui limite considérablement son utilité. Envie de venir discuter?
DeadMG

@DeadMG, une définition est limpide. Pas de variables libres. Je considère tout langage "assez fonctionnel" s'il est possible de définir le combinateur Y (correctement, pas à votre façon). Un grand "+" est s'il est possible de le définir via des combinateurs S, K et I. Sinon, le langage n'est pas assez expressif.
SK-logic

4
@ SK-logic: Pourquoi ne venez- vous pas sur le chat , comme l'a demandé le gentil modérateur? En outre, un combinateur Y est un combinateur Y, il fait le travail ou non. La version Haskell du combinateur Y est fondamentalement exactement la même que celle-ci, c'est juste que l'état exprimé vous est caché.
DeadMG

11

La « liberté d'avoir à se soucier de libérer des ressources » que les langues recueillies fournissent déchets soi - disant est une importante mesure une illusion. Continuez à ajouter des éléments dans une carte sans jamais en supprimer, et vous comprendrez bientôt de quoi je parle.

En fait, les fuites de mémoire sont assez fréquentes dans les programmes écrits dans des langages GCed, car ces langages ont tendance à rendre les programmeurs paresseux et à leur faire acquérir un faux sentiment de sécurité que le langage prendra toujours en quelque sorte (comme par magie) soin de chaque objet qu'ils ne souhaite plus avoir à y penser.

La récupération de place est simplement une facilité nécessaire pour les langages qui ont un autre objectif plus noble: tout traiter comme un pointeur vers un objet, et en même temps cacher au programmeur le fait qu'il s'agit d'un pointeur, afin que le programmeur ne puisse pas commettre suicide en tentant l'arithmétique des pointeurs et similaires. Tout étant un objet signifie que les langages GCed doivent allouer des objets beaucoup plus souvent que les langages non GCed, ce qui signifie que s'ils mettent le fardeau de la désallocation de ces objets sur le programmeur, ils seraient extrêmement peu attrayants.

En outre, la récupération de place est utile afin de fournir au programmeur la possibilité d'écrire du code serré, de manipuler des objets à l'intérieur des expressions, de manière fonctionnelle, sans avoir à décomposer les expressions en instructions distinctes afin de permettre la désallocation de chaque objet unique qui participe à l'expression.

Mis à part tout cela, s'il vous plaît noter que dans le début de ma réponse , j'ai écrit « il est une importante mesure une illusion ». Je n'ai pas écrit que c'était une illusion. Je n'ai même pas écrit que c'est surtout une illusion. La collecte des ordures est utile pour enlever au programmeur la tâche subalterne de s'occuper de la désallocation de ses objets. Donc, dans ce sens, c'est une fonctionnalité de productivité.


4

Le garbage collector ne corrige aucun "bogue". C'est une partie nécessaire de certaines sémantiques de langues de haut niveau. Avec un GC, il est possible de définir des niveaux d'abstractions plus élevés, tels que les fermetures lexicales et similaires, tandis qu'avec une gestion manuelle de la mémoire, ces abstractions seront fuyantes, inutilement liées aux niveaux inférieurs de la gestion des ressources.

Un "principe de propriété unique", mentionné dans les commentaires, est un assez bon exemple d'une telle abstraction qui fuit. Un développeur ne devrait pas du tout se préoccuper du nombre de liens vers une instance de structure de données élémentaire particulière, sinon tout morceau de code ne serait pas générique et transparent sans un grand nombre de limitations et d'exigences supplémentaires (non directement visibles dans le code lui-même) . Un tel code ne peut pas être composé en un code de niveau supérieur, ce qui constitue une violation intolérable du principe de séparation des couches de responsabilité (un élément constitutif majeur de l'ingénierie logicielle, malheureusement pas du tout respecté par la plupart des développeurs de bas niveau).


1
@Mason Wheeler, même C ++ implémente une forme très limitée de fermetures. Mais ce n'est pas vraiment une fermeture appropriée, généralement utilisable.
SK-logic

1
Vous vous trompez. Aucun GC ne peut vous protéger du fait que vous ne pouvez pas faire référence à des variables de pile. Et c'est drôle - en C ++, vous pouvez également utiliser l'approche "Copier un pointeur vers une variable allouée dynamiquement qui sera détruite de manière appropriée et automatique".
DeadMG

1
@DeadMG, ne voyez-vous pas que votre code fuit des entités de bas niveau à travers tout autre niveau que vous construisez en haut?
SK-logic

1
@ SK-Logic: OK, nous avons un problème de terminologie. Quelle est votre définition de la «fermeture réelle» et que peuvent-ils faire que les fermetures de Delphi ne puissent pas faire? (Et l'inclusion de tout ce qui concerne la gestion de la mémoire dans votre définition déplace les messages d'objectif. Parlons du comportement, pas des détails d'implémentation.)
Mason Wheeler

1
@ SK-Logic: ... et avez-vous un exemple de quelque chose qui peut être fait avec des fermetures lambda simples et non typées que les fermetures de Delphi ne peuvent pas accomplir?
Mason Wheeler

2

Vraiment, la gestion de votre propre mémoire n'est qu'une autre source potentielle de bugs.

Si vous oubliez un appel à free(ou quel que soit l'équivalent dans la langue que vous utilisez), votre programme peut passer tous ses tests, mais une fuite de mémoire. Et dans un programme moyennement complexe, il est assez facile d'oublier un appel à free.


3
Manqué freen'est pas la pire chose. Tôt freeest beaucoup plus dévastateur.
Herby

2
Et le double free!
quant_dev

Hehe! J'accepterais les deux commentaires ci-dessus. Je n'ai jamais commis une de ces transgressions moi-même (pour autant que je sache), mais je peux voir à quel point les effets peuvent être terribles. La réponse de quant_dev dit tout: les erreurs d'allocation et de désallocation de mémoire sont notoirement difficiles à trouver et à corriger.
Dawood dit de réintégrer Monica le

1
Ceci est une erreur. Vous comparez "début 1970" à "fin 1990". Les GC qui existaient à l'époque mallocet qui freeétaient la voie non GC étaient beaucoup trop lents pour être utiles à quoi que ce soit. Vous devez le comparer à une approche moderne non GC, comme RAII.
DeadMG

2
@DeadMG RAII n'est pas une gestion manuelle de la mémoire
quant_dev

2

La ressource manuelle est non seulement fastidieuse, mais également difficile à déboguer. En d'autres termes, non seulement il est fastidieux de bien faire les choses, mais aussi lorsque vous vous trompez, il n'est pas évident de savoir où se situe le problème. En effet, contrairement à la division par exemple par zéro, les effets de l'erreur apparaissent loin de la source d'erreur, et la connexion des points nécessite du temps, de l'attention et de l'expérience.


1

Je pense que la collecte des ordures mérite beaucoup pour les améliorations linguistiques qui n'ont rien à voir avec GC, à part faire partie d'une grande vague de progrès.

Le seul avantage solide de GC que je connaisse est que vous pouvez libérer un objet dans votre programme et savoir qu'il disparaîtra lorsque tout le monde en aura fini. Vous pouvez le passer à la méthode d'une autre classe et ne pas vous en soucier. Vous ne vous souciez pas des autres méthodes auxquelles il est transmis ou des autres classes qui le référencent. (Les fuites de mémoire sont de la responsabilité de la classe référençant un objet, pas de la classe qui l'a créé.)

Sans GC, vous devez suivre tout le cycle de vie de la mémoire allouée. Chaque fois que vous passez une adresse vers le haut ou vers le bas à partir du sous-programme qui l'a créée, vous avez une référence hors de contrôle à cette mémoire. Dans le mauvais vieux temps, même avec un seul thread, la récursivité et un système d'exploitation (Windows NT) m'ont empêché de contrôler l'accès à la mémoire allouée. J'ai dû truquer la méthode gratuite dans mon propre système d'allocation pour conserver les blocs de mémoire pendant un certain temps jusqu'à ce que toutes les références soient effacées. Le temps d'attente était une pure conjecture, mais cela a fonctionné.

C'est donc le seul avantage GC que je connaisse, mais je ne pourrais pas vivre sans. Je ne pense pas qu'une sorte de POO vole sans lui.


1
Juste au-dessus de ma tête, Delphi et C ++ ont tous deux été assez réussis en tant que langages OOP sans GC. Tout ce dont vous avez besoin pour éviter les «références hors de contrôle» est un peu de discipline. Si vous comprenez le principe de propriété unique, (voir ma réponse), les problèmes dont vous parlez ici deviennent des non-problèmes totaux.
Mason Wheeler

@MasonWheeler: Lorsqu'il est temps de libérer l'objet propriétaire, il doit connaître tous les emplacements auxquels ses objets appartenant sont référencés. Conserver ces informations et les utiliser pour supprimer les références me semble un travail énorme. J'ai souvent trouvé que les références ne pouvaient pas encore être effacées. J'ai dû marquer le propriétaire comme supprimé, puis lui donner vie périodiquement pour voir s'il pouvait se libérer en toute sécurité. Je n'ai jamais utilisé Delphi, mais pour un petit sacrifice dans l'efficacité d'exécution, C # / Java m'a donné un gros coup de pouce en temps de développement par rapport à C ++. (Tout n'est pas dû à GC, mais cela a aidé.)
RalphChapin

1

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 freepour une donnée malloc. Cela arrive vraiment.

Cependant, il existe des outils comme valgrindla 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 freen'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 freemé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.

entrez la description de l'image ici

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.

entrez la description de l'image ici

... 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).

entrez la description de l'image ici

... 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:

entrez la description de l'image ici

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:

entrez la description de l'image ici

Maintenant, cela nous laisserait normalement des pointeurs pendants partout, alors retirons les pointeurs à Joe.

entrez la description de l'image ici

... 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.


-1

Voici une liste des problèmes rencontrés par les programmeurs C ++ lorsqu'ils traitent avec la mémoire:

  1. Le problème d'étendue se produit dans la mémoire allouée à la pile: sa durée de vie ne s'étend pas en dehors de la fonction dans laquelle elle a été allouée. .
  2. Le problème de taille est dans la pile allouée et l'allocation à partir de l'intérieur de l'objet et de la mémoire allouée en partie au tas: la taille du bloc de mémoire ne peut pas changer lors de l'exécution. Les solutions sont des matrices de mémoire de tas, des pointeurs, des bibliothèques et des conteneurs.
  3. Le problème d'ordre de définition est lors de l'allocation à l'intérieur des objets: les classes à l'intérieur du programme doivent être dans le bon ordre. Les solutions limitent les dépendances à une arborescence et réorganisent les classes et n'utilisent pas de déclarations avancées, de pointeurs et de mémoire de segment et n'utilisent pas de déclarations directes.
  4. Le problème Inside-Outside est dans la mémoire allouée aux objets. L'accès à la mémoire à l'intérieur des objets est divisé en deux parties, une partie de la mémoire à l'intérieur d'un objet et une autre mémoire à l'extérieur de celui-ci, et les programmeurs doivent choisir correctement d'utiliser la composition ou les références en fonction de cette décision. Les solutions prennent la décision correctement, ou les pointeurs et la mémoire de tas.
  5. Le problème des objets récursifs est dans la mémoire allouée aux objets. La taille des objets devient infinie si le même objet est placé à l'intérieur de lui-même et que les solutions sont des références, de la mémoire de tas et des pointeurs.
  6. Le problème de suivi de la propriété est dans la mémoire allouée au tas, le pointeur contenant l'adresse de la mémoire allouée au tas doit être passé du point d'allocation au point de désallocation. Les solutions sont la mémoire allouée à la pile, la mémoire allouée aux objets, les conteneurs auto_ptr, shared_ptr, unique_ptr, stdlib.
  7. Le problème de duplication de propriété est dans la mémoire allouée en tas: la désallocation ne peut être effectuée qu'une seule fois. Les solutions sont la mémoire allouée par pile, la mémoire allouée aux objets, auto_ptr, shared_ptr, unique_ptr, les conteneurs stdlib.
  8. Le problème du pointeur nul est dans la mémoire allouée au tas: les pointeurs sont autorisés à être NULL, ce qui fait que la plupart des opérations se bloquent à l'exécution. Les solutions sont la mémoire de pile, la mémoire allouée aux objets et une analyse minutieuse des zones de tas et des références.
  9. Le problème de fuite de mémoire est dans la mémoire allouée au tas: Oublier d'appeler delete pour chaque bloc de mémoire alloué. Les solutions sont des outils comme valgrind.
  10. Le problème de débordement de pile concerne les appels de fonction récursifs qui utilisent la mémoire de pile. Normalement, la taille de la pile est complètement déterminée au moment de la compilation, à l'exception du cas des algorithmes récursifs. La définition incorrecte de la taille de la pile du système d'exploitation provoque également souvent ce problème car il n'y a aucun moyen de mesurer la taille requise de l'espace de pile.

Comme vous pouvez le voir, la mémoire de tas résout de nombreux problèmes existants, mais elle entraîne une complexité supplémentaire. GC est conçu pour gérer une partie de cette complexité. (désolé si certains noms de problème ne sont pas les noms corrects pour ces problèmes - il est parfois difficile de trouver le nom correct)


1
-1: Pas de réponse à la question.
Sjoerd
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.