64 votes

Pourquoi pas de comptage de référence + collecte des ordures en C#?

Je viens d'un environnement C++ et je travaille avec C# depuis environ un an. Comme beaucoup d'autres, je suis perplexe quant au fait que la gestion déterministe des ressources ne soit pas intégrée au langage. Au lieu de destructeurs déterministes, nous avons le modèle de disposable. Les gens commencent à se demander s'il vaut la peine de propager le cancer IDisposable dans leur code.

Dans mon cerveau biaisé par le C++, il semble qu'utiliser des pointeurs intelligents avec comptage de références et destructeurs déterministes soit un grand pas en avant par rapport à un ramasse-miettes qui nécessite l'implémentation de IDisposable et l'appel de Dispose pour nettoyer vos ressources autres que la mémoire. Admettons, je ne suis pas très intelligent... donc je pose la question uniquement dans le but de mieux comprendre pourquoi les choses sont comme elles sont.

Et si C# était modifié de manière à ce que :

Les objets soient comptés par référence. Lorsque le nombre de références d'un objet atteint zéro, une méthode de nettoyage des ressources est appelée de manière déterministe sur l'objet, puis l'objet est marqué pour le ramasse-miettes. Le ramassage des ordures se produit à un moment non déterministe à l'avenir, moment où la mémoire est récupérée. Dans ce scénario, vous n'avez pas à implémenter IDisposable ou à vous souvenir d'appeler Dispose. Vous implémentez simplement la fonction de nettoyage des ressources si vous avez des ressources autres que la mémoire à libérer.

  • Pourquoi est-ce une mauvaise idée?
  • Cela ne contredirait-il pas le but du ramasse-miettes?
  • Serait-il possible de mettre en œuvre une telle chose?

ÉDIT: D'après les commentaires jusqu'à présent, c'est une mauvaise idée car

  1. Le ramasse-miettes est plus rapide sans comptage de références
  2. Problème de gestion des cycles dans le graphe d'objets

Je pense que le premier point est valable, mais le deuxième est facile à résoudre en utilisant des références faibles.

Alors, est-ce que l'optimisation de la vitesse l'emporte sur les inconvénients que vous:

  1. pourriez ne pas libérer une ressource non mémoire en temps opportun
  2. pourriez libérer une ressource non mémoire trop tôt

Si votre mécanisme de nettoyage des ressources est déterministe et intégré au langage, vous pouvez éliminer ces possibilités.

5 votes

Apple vient de annoncer que Objective C dans iOS 5 prendra en charge le "Comptage automatique des références". Tous les pointeurs Objective C sont automatiquement comptés et conservés/libérés. Cependant, Objective C prend toujours en charge la méthode dealloc qui vous donne un "destructeur" déterministe où vous pouvez libérer des ressources non liées à la mémoire et vous n'avez pas à utiliser IDisposable. Ils ont spécifiquement déclaré qu'ils ne prendront pas en charge un GC pour Objective C. Cela me semble être la bonne approche. À mon avis, Java et .NET se trompent sur ce point.

4 votes

Une chose à laquelle je m'inquiète en ce qui concerne GC est que les abonnés aux événements, même en mettant en œuvre des modèles d'événements faibles, continuent de recevoir des notifications tout en attendant d'être collectés par le ramasse-miettes. Cela rend obligatoire la mise en œuvre de IDisposable dans toutes les classes d'abonnés aux événements.

1 votes

Oui, et de la réponse acceptée : "Nous pensons qu'il est très important de résoudre le problème de cycle sans contraindre les programmeurs à comprendre, traquer et concevoir autour de ces problèmes de structures de données complexes." J'ai été rappelé de cela aujourd'hui alors que j'étais obligé de comprendre, traquer et concevoir autour des interactions complexes entre les structures de données et la durée de vie des objets afin de comprendre pourquoi la mémoire n'était pas libérée. Quelqu'un a oublié de disposer d'un objet qui se désabonne d'un gestionnaire d'événements global dans Dispose(). Les Extensions Réactives rendent tout jetable.

54voto

Lucas Points 10415

Brad Abrams a publié un e-mail de Brian Harry écrit pendant le développement du framework .Net. Il détaille bon nombre des raisons pour lesquelles le comptage des références n'a pas été utilisé, même lorsque l'une des priorités initiales était de maintenir une équivalence sémantique avec VB6, qui utilise le comptage des références. Il examine des possibilités telles que le fait d'avoir certains types comptés par référence et d'autres non (IRefCounted!), ou de compter par référence des instances spécifiques, et pourquoi aucune de ces solutions n'a été jugée acceptable.

Parce que [la question de la gestion des ressources et de la finalisation déterministe] est un sujet si sensible, je vais essayer d'être aussi précis et complet que possible dans mon explication. Je m'excuse pour la longueur du mail. Les 90 % de ce mail tentent de vous convaincre que le problème est vraiment difficile. Dans la dernière partie, je parlerai des choses que nous essayons de faire, mais vous avez besoin de la première partie pour comprendre pourquoi nous examinons ces options.

...

Nous avons initialement pensé que la solution prendrait la forme d'un comptage automatique des références (afin que le programmeur ne puisse pas oublier), plus quelques autres éléments pour détecter et gérer automatiquement les cycles. ...nous avons finalement conclu que cela n'allait pas fonctionner dans le cas général.

...

En résumé :

  • Nous estimons qu'il est très important de résoudre le problème des cycles sans obliger les programmeurs à comprendre, suivre et concevoir des solutions pour ces problèmes complexes de structure de données.
  • Nous voulons nous assurer d'avoir un système haute performance (à la fois en vitesse et en taille de travail) et notre analyse montre que l'utilisation du comptage des références pour chaque objet du système ne nous permettra pas d'atteindre cet objectif.
  • Pour diverses raisons, y compris des problèmes de composition et de conversion, il n'y a pas de solution transparente simple pour ne compter par référence que les objets qui en ont besoin.
  • Nous avons choisi de ne pas opter pour une solution qui offre une finalisation déterministe pour un seul langage/contexte car cela entrave l'interopérabilité avec d'autres langues et provoque une scission des bibliothèques de classes en créant des versions spécifiques à chaque langage.

7 votes

Certainement lire le courriel complet - il explique en détail les raisons derrière la décision.

3 votes

Microsoft a cassé tous leurs liens de blog. Je pense que c'est le nouveau domicile de l'article sur la gestion des ressources lié dans cette réponse. Cela m'a pris un certain temps pour trouver.

31voto

Gishu Points 59012

Le collecteur d'ordures ne nécessite pas que vous écriviez une méthode Dispose pour chaque classe/type que vous définissez. Vous en définissez une seule lorsque vous devez explicitement effectuer un nettoyage ; lorsque vous avez explicitement alloué des ressources natives. La plupart du temps, le GC ne fait que récupérer la mémoire même si vous ne faites que quelque chose comme new() sur un objet.

Le GC utilise le comptage de références - cependant il le fait d'une manière différente en trouvant quels objets sont 'atteignables' (Ref Count > 0) à chaque fois qu'il effectue une collection... il ne le fait simplement pas de manière compteur entier. Les objets inaccessibles sont collectés (Ref Count = 0). De cette façon, le moteur d'exécution n'a pas à faire de la maintenance/mise à jour des tables à chaque fois qu'un objet est attribué ou libéré... devrait être plus rapide.

La seule différence majeure entre C++ (déterministe) et C# (non-déterministe) est quand l'objet serait nettoyé. Vous ne pouvez pas prédire le moment exact où un objet serait collecté en C#.

Énième recommandation : Je vous recommande de lire le chapitre de Jeffrey Richter sur la GC dans CLR via C# au cas où vous seriez vraiment intéressé par le fonctionnement du GC.

3 votes

Alors, évitez RAII car le comptage des références est plus lent et vous n'avez pas à gérer Dispose sur chaque classe. Cela me semble être un mauvais compromis.

2 votes

N'a pas compris votre commentaire ici... Je viens d'expliquer que la mise en œuvre de Dispose n'est pas obligatoire pour chaque type/classe C#. Le GC fonctionne parfaitement même si aucune de vos classes n'a implémenté Dispose.

1 votes

Je ne suis pas sûr que ma question ait été entièrement comprise. Beaucoup de classes dans le framework implémentent IDisposable. Vous devez appeler dispose sur ces classes. Ce serait bien si le concept RAII était possible pour aider avec ça. Quoi qu'il en soit, il semble que la plupart des gens soient d'accord avec votre raisonnement.

23voto

user8032 Points 758

Le comptage de références a été essayé en C#. Je crois que les personnes qui ont publié Rotor (une implémentation de référence de CLR pour laquelle le code source était disponible) ont utilisé un GC basé sur le comptage de références juste pour voir comment cela se comparerait à la version générique. Le résultat était surprenant - le GC "standard" était tellement plus rapide que ce n'était même pas drôle. Je ne me souviens pas exactement où j'ai entendu cela, je pense que c'était dans l'un des podcasts de Hanselminutes. Si vous voulez voir le C++ se faire littéralement écraser en termes de performances par rapport au C# - recherchez l'application de dictionnaire chinois de Raymond Chen. Il a créé une version en C++, puis Rico Mariani en a fait une en C#. Je crois qu'il a fallu à Raymond 6 itérations pour finalement battre la version C#, mais à ce moment-là, il a dû abandonner toute la belle orientation objet du C++, et se mettre au niveau de l'API win32. L'ensemble est devenu une astuce de performance. Le programme C#, quant à lui, n'a été optimisé qu'une seule fois, et à la fin ressemblait toujours à un projet OO décent

4 votes

Le comptage de références est connu pour être globalement plus lent que les GC par traçage. Cependant, il a l'avantage de temps de pause maximum plus courts, et la mémoire sera souvent libérée dès que possible.

6 votes

De mémoire, je ne pense pas que le temps d'exécution du Rotor était très optimisé, donc je ne pense pas que c'est un très bon point de données. +1 pour la référence de l'application de dictionnaire qui était intéressante. Voici le lien pour référence: blogs.msdn.com/ricom/archive/2005/05/10/416151.aspx

0 votes

J'ai besoin de souligner qu'une quantité non négligeable d'efforts que M. Chen a dû déployer était due au fait que la bibliothèque standard C++ (ou peut-être juste la plupart des implémentations actuelles) est médiocre en termes de performances (conçue pour un "optimiseur suffisamment intelligent") - il serait parfaitement possible d'écrire un programme aussi efficace en style orienté objet.

15voto

Luke Quinane Points 8257

Il y a une différence entre le comptage de références du pointeur intelligent de style C++ et la collecte des déchets par comptage de références. J'en ai également parlé sur mon blog mais voici un résumé rapide :

Comptage de références de style C++ :

  • Coût non borné à la diminution : si la racine d'une grande structure de données est diminuée à zéro, il y a un coût non borné pour libérer toutes les données.

  • Collecte de cycle manuelle : pour empêcher les fuites de mémoire des structures de données cycliques, le programmeur doit manuellement rompre toutes les structures potentielles en remplaçant une partie du cycle par un pointeur intelligent faible. C'est une autre source de défauts potentiels.

Collecte de déchets par comptage de références

  • RC différé : les modifications du compteur de références d'un objet sont ignorées pour les références de pile et de registre. Au lieu de cela, lorsque le GC est déclenché, ces objets sont conservés en collectant un ensemble racine. Les modifications du compteur de références peuvent être différées et traitées par lots. Cela entraîne un débit plus élevé.

  • Fusion : en utilisant une barrière d'écriture, il est possible de fusionner les modifications du compteur de références. Cela permet d'ignorer la plupart des modifications du compteur de références d'objets, améliorant les performances de RC pour les références fréquemment mutées.

  • Détection de cycle : pour une implémentation complète de GC, un détecteur de cycles doit également être utilisé. Cependant, il est possible d'effectuer une détection de cycle de manière incrémentielle, ce qui signifie à son tour un temps de GC borné.

En gros, il est possible de mettre en œuvre un collecteur de déchets basé sur un RC à hautes performances pour des environnements d'exécution tels que la JVM de Java et l'environnement d'exécution CLR de .net.

Je pense que les collecteurs par traçage sont en partie utilisés pour des raisons historiques : bon nombre des améliorations récentes du comptage de références sont survenues après la sortie de la JVM et de l'environnement d'exécution .net. Le travail de recherche prend également du temps pour passer aux projets de production.

Élimination des ressources de manière déterministe

C'est pratiquement une question à part. L'environnement d'exécution .net le rend possible en utilisant l'interface IDisposable, exemple ci-dessous. J'aime aussi la réponse de Gishu.


@Skrymsli, c'est le but du mot-clé "using". Par exemple :

public abstract class BaseCriticalResource : IDiposable {
    ~ BaseCriticalResource () {
        Dispose(false);
    }
    public void Dispose() {
        Dispose(true);
        GC.SuppressFinalize(this); // Pas besoin d'appeler le finaliseur maintenant
    }
    protected virtual void Dispose(bool disposing) { }
}

Ensuite, pour ajouter une classe avec une ressource critique :

public class ComFileCritical : BaseCriticalResource {
    private IntPtr nativeResource;
    protected override Dispose(bool disposing) {
        // libérer les ressources natives s'il y en a.
        if (nativeResource != IntPtr.Zero) {
            ComCallToFreeUnmangedPointer(nativeResource);
            nativeResource = IntPtr.Zero;
        }
    }
}

Ensuite, son utilisation est aussi simple que :

using (ComFileCritical fileResource = new ComFileCritical()) {
    // Quelques actions sur fileResource
}

// Les ressources critiques de fileResource sont libérées à ce moment-là

Voir aussi implémenter correctement IDisposable.

2 votes

Intéressant blog post... Je suis moins préoccupé par la libération de la mémoire que par la libération des ressources non-mémoire qui sont potentiellement critiques en termes de temps. (Pensez à un pointeur vers un objet COM qui détient un verrou de fichier. Vous voulez que ce verrou soit libéré lorsque vous en avez terminé avec lui, pas lorsque le GC exécute un finaliseur ultérieurement.) Je pense qu'il doit y avoir une combinaison de GC et de pointeurs intelligents qui offre le meilleur des deux mondes où vous avez une gestion exceptionnelle de la mémoire et pourtant les ressources critiques peuvent être libérées de manière déterministe sans imposer un fardeau excessif au programmeur.

0 votes

Lorsque je demande cela, ce n'est pas parce que je ne sais pas comment utiliser ou la bonne manière de disposer des ressources. Je trouve simplement que c'est MAL que nous devions le faire du tout ! Habituellement, cela fonctionne comme vous l'avez décrit ci-dessus. Que se passe-t-il si vous avez besoin de conserver cette ressource ? Vous devez maintenant implémenter IDisposable. De plus, le programmeur doit savoir envelopper le contenu dans la construction using. (Je sais, utilisez FXCop !) C'est juste du sucre syntaxique pour appeler free, release, dispose. Le compilateur devrait le savoir. C'est le principal avantage de l'idiome RAII en C++. Désolé, je suis probablement juste bête.

0 votes

@Skrymsli tu n'es pas seul dans ta "bêtise", je l'ai aussi suggéré il y a plusieurs années : dotnet247.com/247reference/msgs/26/131667.aspx

5voto

Unknown Points 22789

Je sais quelque chose sur la collecte des déchets. Voici un bref résumé car une explication complète dépasse le cadre de cette question.

.NET utilise un collecteur de déchets générationnel de copie et de compactage. Cela est plus avancé que le décompte de références et a l'avantage de pouvoir collecter des objets qui se réfèrent à eux-mêmes soit directement, soit à travers une chaîne.

Le décompte de références ne collectera pas les cycles. Le décompte de références a également un débit plus faible (plus lent globalement) mais avec l'avantage de pauses plus rapides (les pauses maximales sont plus petites) qu'un collecteur de traçage.

1 votes

Ce point pourrait être discuté mais il semble plus facile d'éviter un cycle dans un système à comptage de références en utilisant des références faibles que d'éviter les fuites de ressources ou la libération différée avec IDisposable.

1 votes

@Skrymsli en réalité, ce point ne peut pas être discuté. Parfois, vous ne savez pas quand vous allez créer un cycle de référence.

1 votes

Seules les implémentations naïves du comptage des références ne collecteront pas les cycles. Un comptage de références à haute performance est également possible pour des environnements d'exécution tels que .net : cs.anu.edu.au/~Steve.Blackburn/pubs/papers/urc-oopsla-2003.pdf

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X