web-dev-qa-db-fra.com

Quand est-ce une bonne idée de forcer la collecte des ordures?

Je lisais donc ne question sur le fait de forcer le ramasse-miettes C # à s'exécuter où presque chaque réponse est la même: vous pouvez le faire, mais vous ne devriez pas - sauf pour certains cas très rares . Malheureusement, personne n'y explique ce que sont de tels cas.

Pouvez-vous me dire dans quel genre de scénario c'est en fait une bonne ou une bonne idée de forcer la collecte des ordures?

Je ne demande pas des cas spécifiques C # mais plutôt tous les langages de programmation qui ont un garbage collector. Je sais que vous ne pouvez pas forcer GC sur tous les langages, comme Java, mais supposons que vous le puissiez.

138
Omega

Vous ne pouvez vraiment pas faire de déclarations générales sur la manière appropriée d'utiliser toutes les implémentations GC . Ils varient énormément. Je vais donc parler du fichier .NET dont vous avez parlé à l'origine.

Vous devez connaître le comportement du GC assez intimement pour le faire avec n'importe quelle logique ou raison.

Le seul conseil que je puisse donner sur la collecte est: Ne le faites jamais.

Si vous connaissez vraiment les détails complexes du GC, vous n'aurez pas besoin de mes conseils, donc cela n'aura pas d'importance. Si vous ne le savez pas déjà avec 100% de confiance, cela vous aidera et vous devrez chercher en ligne et trouver une réponse comme celle-ci: Vous ne devriez pas appeler GC.Collect, ou bien: Vous devriez aller apprendre les détails du fonctionnement du GC à l'intérieur et à l'extérieur, et c'est seulement alors que vous connaître la réponse.

Il y a un endroit sûr, il est logique d'utiliser GC.Collect:

GC.Collect est une API disponible que vous pouvez utiliser pour profiler les temps des choses. Vous pouvez profiler un algorithme, collecter et profiler un autre algorithme immédiatement après en sachant que le GC du premier algo ne se produisait pas lors de votre second faussant les résultats.

Ce type de profilage est la seule fois que je suggérerais de collecter manuellement à quiconque.


Exemple artificiel quand même

Un cas d'utilisation possible est que si vous chargez des choses très volumineuses, elles se retrouveront dans le grand tas d'objets qui ira directement à la génération 2, bien que là encore la génération 2 soit destinée aux objets à longue durée de vie car elle se collecte moins fréquemment. Si vous savez que vous chargez des objets à vie courte dans Gen 2 pour une raison quelconque, vous pouvez les effacer plus rapidement pour garder votre Gen 2 plus petit et c'est collections plus rapidement.

C'est le meilleur exemple que je pourrais trouver, et ce n'est pas bon - la pression LOH que vous créez ici provoquerait des collections plus fréquentes, et les collections sont si fréquentes qu'elles le sont - il y a de fortes chances qu'il efface la LOH tout comme rapide comme vous le souffliez avec des objets temporaires. Je ne fais tout simplement pas confiance à moi-même pour présumer une meilleure fréquence de collecte que le GC lui-même - réglé par des gens loin loin plus intelligent que moi.


Parlons donc de la sémantique et des mécanismes dans le GC .NET ... ou ..

Tout ce que je pense que je connais sur le GC .NET

S'il vous plaît, toute personne qui trouve des erreurs ici - corrigez-moi. Une grande partie du GC est bien connue pour être de la magie noire et bien que j'aie essayé de laisser de côté les détails dont j'étais incertain, je me suis probablement encore trompé.

Ci-dessous, il manque délibérément de nombreux détails dont je ne suis pas sûr, ainsi qu'un ensemble d'informations beaucoup plus vaste dont je ne suis tout simplement pas au courant. Utilisez ces informations à vos risques et périls.


Concepts GC

Le GC .NET se produit à des moments incohérents, c'est pourquoi il est appelé "non déterministe", ce qui signifie que vous ne pouvez pas compter sur lui pour se produire à des moments spécifiques. C'est également un garbage collector générationnel, ce qui signifie qu'il partitionne vos objets en combien de passages GC ils ont vécu.

Les objets du tas de génération 0 ont vécu 0 collections, celles-ci ont été nouvellement créées, donc récemment aucune collection ne s'est produite depuis leur instanciation. Les objets de votre segment de mémoire Gen 1 ont vécu une passe de collecte, et de la même manière les objets de votre segment Gen 2 ont traversé 2 passes de collecte.

Maintenant, il convient de noter la raison pour laquelle il qualifie ces générations et partitions spécifiques en conséquence. Le GC .NET ne reconnaît que ces trois générations, car les passes de collecte qui parcourent ces trois tas sont toutes légèrement différentes. Certains objets peuvent survivre à la collecte des milliers de fois. Le GC laisse simplement ceux-ci de l'autre côté de la partition de tas Gen 2, il n'y a aucun intérêt à les partitionner ailleurs car ils sont en fait Gen 44; le passage de collecte sur eux est le même que tout dans le tas de génération 2.

Il existe des objectifs sémantiques pour ces générations spécifiques, ainsi que des mécanismes mis en œuvre qui les respectent, et j'y reviendrai dans un instant.


Contenu d'une collection

Le concept de base d'une passe de collecte GC est qu'il vérifie chaque objet dans un espace de tas pour voir s'il existe encore des références actives (racines GC) à ces objets. Si une racine GC est trouvée pour un objet, cela signifie que le code en cours d'exécution peut toujours atteindre et utiliser cet objet, il ne peut donc pas être supprimé. Cependant, si une racine GC n'est pas trouvée pour un objet, cela signifie que le processus en cours d'exécution n'a plus besoin de l'objet, il peut donc le supprimer pour libérer de la mémoire pour de nouveaux objets.

Maintenant, une fois qu'il a fini de nettoyer un tas d'objets et d'en laisser certains, il y aura un effet secondaire malheureux: des espaces libres entre les objets vivants où les morts ont été retirés. Cette fragmentation de la mémoire, si elle était laissée seule, gaspillerait simplement de la mémoire, donc les collections font généralement ce qu'on appelle le "compactage" où elles prennent tous les objets vivants laissés et les pressent ensemble dans le tas afin que la mémoire libre soit contiguë d'un côté du tas pour Gen 0.

Maintenant, étant donné l'idée de 3 tas de mémoire, tous partitionnés par le nombre de passes de collecte qu'ils ont vécu, parlons de la raison pour laquelle ces partitions existent.


Collection Gen 0

Gen 0 étant les objets les plus récents absolus, a tendance à être très petit - vous pouvez donc les collecter en toute sécurité très fréquemment . La fréquence garantit que le tas reste petit et que les collections sont très rapides car elles collectent sur un si petit tas. Ceci est basé plus ou moins sur une heuristique qui prétend: Une grande majorité des objets temporaires que vous créez, sont très temporaire, donc temporaire ils ne seront plus utilisés ou référencés presque immédiatement après leur utilisation, et pourront donc être collectés.


Collection Gen 1

La Gen 1 étant des objets qui ne tombent pas dans cette catégorie très temporaire d'objets, peut encore être de courte durée, car encore une grande partie de la les objets créés ne sont pas utilisés longtemps. Par conséquent, Gen 1 collecte également assez fréquemment, ce qui réduit encore la taille du tas afin que les collections soient rapides. Cependant, l'hypothèse est moins de ses objets sont temporaires que Gen 0, donc il recueille moins fréquemment que Gen 0

Je dirai franchement que je ne connais pas les mécanismes techniques qui diffèrent entre la passe de collecte de la génération 0 et la génération 1, s'il en existe d'autres que la fréquence qu'ils collectent.


Collection Gen 2

Gen 2 doit maintenant être la mère de tous les tas, non? Eh bien, oui, c'est plus ou moins vrai. C'est là que vivent tous vos objets permanents - l'objet dans lequel votre Main() vit par exemple, et tout ce que Main() référence parce que ceux-ci seront enracinés jusqu'à ce que votre Main() revienne à la fin de votre processus.

Étant donné que Gen 2 est un seau pour pratiquement tout ce que les autres générations ne pouvaient pas collecter, ses objets sont en grande partie permanents, ou ont au moins une longue durée de vie. Donc, reconnaître très peu de ce qui se trouve dans Gen 2 sera en fait quelque chose qui peut être collecté, il n'a pas besoin d'être collecté fréquemment. Cela permet à sa collection d'être également plus lente, car elle s'exécute beaucoup moins fréquemment. C'est donc essentiellement là qu'ils ont abordé tous les comportements supplémentaires pour les scénarios étranges, car ils ont le temps de les exécuter.


Gros tas d'objets

Un exemple des comportements supplémentaires de Gen 2 est qu'il fait également la collecte sur le tas d'objets volumineux. Jusqu'à présent, je parlais entièrement du tas de petits objets, mais le runtime .NET alloue des choses de certaines tailles à un tas distinct en raison de ce que j'ai appelé le compactage ci-dessus. Le compactage nécessite de déplacer des objets lorsque les collections se terminent sur le tas de petits objets. S'il y a un objet vivant de 10 Mo dans Gen 1, cela prendra beaucoup plus de temps pour terminer le compactage après la collecte, ralentissant ainsi la collecte de Gen 1. Donc, cet objet de 10 Mo est alloué au grand tas d'objets et collecté pendant la deuxième génération, qui s'exécute si rarement.


Finalisation

Un autre exemple est les objets avec finaliseurs. Vous placez un finaliseur sur un objet qui référence des ressources au-delà de la portée de .NETs GC (ressources non managées). Le finaliseur est le seul moyen par lequel le GC peut demander qu'une ressource non gérée soit collectée - vous implémentez votre finaliseur pour effectuer la collecte/suppression/libération manuelle de la ressource non gérée pour vous assurer qu'elle ne fuit pas de votre processus. Lorsque le GC arrive à exécuter votre finaliseur d'objets, votre implémentation efface la ressource non gérée, ce qui rend le GC capable de supprimer votre objet sans risquer une fuite de ressources.

Le mécanisme avec lequel les finaliseurs le font est d'être référencé directement dans une file d'attente de finalisation. Lorsque le runtime alloue un objet avec un finaliseur, il ajoute un pointeur sur cet objet dans la file d'attente de finalisation et verrouille votre objet en place (appelé épinglage) afin que le compactage ne le déplace pas, ce qui romprait la référence de la file d'attente de finalisation. Au fur et à mesure des passes de collecte, votre objet finira par ne plus avoir de racine GC, mais la finalisation doit être exécutée avant de pouvoir être collectée. Ainsi, lorsque l'objet est mort, la collection déplace sa référence de la file d'attente de finalisation et y place une référence sur ce que l'on appelle la file d'attente "FReachable". Puis la collection continue. À un autre moment "non déterministe" dans le futur, un thread séparé connu sous le nom de thread Finalizer passera par la file d'attente FReachable, exécutant les finaliseurs pour chacun des objets référencés. Une fois terminée, la file d'attente FReachable est vide et elle a retourné un peu sur l'en-tête de chaque objet qui dit qu'ils n'ont pas besoin de finalisation (ce bit peut également être retourné manuellement avec GC.SuppressFinalize Qui est commun dans Dispose() methods), je soupçonne également d'avoir détaché les objets, mais ne me citez pas là-dessus. La prochaine collection qui se trouvera sur le tas de cet objet se trouvera finalement. Les collections de génération 0 ne prêtent même pas attention aux objets avec ce bit nécessaire à la finalisation, il les promeut automatiquement, sans même vérifier leur racine. Un objet non racine qui doit être finalisé dans Gen 1, sera jeté dans la file d'attente FReachable, mais la collection n'en fait rien d'autre, il vit donc dans Gen 2. De cette façon, tous les objets qui ont un finalizer, et ne pas GC.SuppressFinalize sera collecté dans Gen 2.

126
Jimmy Hoffa

Malheureusement, personne n'y explique ce que sont de tels cas.

Je vais donner quelques exemples. Dans l'ensemble, il est rare que forcer un GC soit une bonne idée, mais cela peut en valoir la peine. Cette réponse est issue de mon expérience avec la littérature .NET et GC. Il devrait bien se généraliser à d'autres plates-formes (au moins celles qui ont un GC important).

  • Benchmarks de différents types. Vous voulez un état de segment de mémoire géré connu quand un test de référence commence afin que le GC ne se déclenche pas de manière aléatoire pendant les tests de performance. Lorsque vous répétez une référence, vous voulez que le même nombre et la même quantité de travail GC soient effectués à chaque répétition.
  • Libération soudaine des ressources. Par exemple, fermer une fenêtre GUI importante ou actualiser un cache (et ainsi libérer l'ancien contenu du cache potentiellement volumineux). Le GC ne peut pas détecter cela car tout ce que vous faites est de définir une référence sur null. Le fait que cet orphelin soit un graphique d'objet entier n'est pas facilement détectable.
  • Version de ressources non gérées qui ont fui. Cela ne devrait jamais arriver, bien sûr, mais j'ai vu des cas où une bibliothèque tierce a divulgué des éléments (tels que des objets COM). Le développeur était parfois obligé d'induire une collection.
  • Applications interactives telles que les jeux. Pendant le jeu, les jeux ont des budgets horaires très stricts par image (60 Hz => 16 ms par image). Afin d'éviter les accrochages, vous avez besoin d'une stratégie pour gérer les GC. L'une de ces stratégies consiste à retarder autant que possible les GC G2 et à les forcer à un moment opportun, comme un écran de chargement ou une scène coupée. Le GC ne peut pas savoir quel est le meilleur moment.
  • Contrôle de latence en général. Certaines applications Web désactivent les GC et exécutent périodiquement une collection G2 tout en étant désactivées de la rotation de l'équilibreur de charge. De cette façon, la latence G2 n'est jamais révélée à l'utilisateur.

Si votre objectif est le débit, plus le GC est rare, mieux c'est. Dans ces cas, forcer une collection ne peut pas avoir un impact positif (sauf pour des problèmes plutôt artificiels tels que l'augmentation de l'utilisation du cache CPU en supprimant les objets morts entrecoupés dans le vivants). La collecte par lots est plus efficace pour tous les collectionneurs que je connais. Pour une application de production en consommation de mémoire en régime permanent, induire un GC n'aide pas.

Les exemples ci-dessus ciblent la cohérence et la délimitation de l'utilisation de la mémoire. Dans ces cas, les GC induits peuvent avoir un sens.

Il semble y avoir une idée largement répandue que le GC est une entité divine qui induit une collection chaque fois qu'il est effectivement optimal de le faire. Aucun GC que je connaisse n'est aussi sophistiqué et en effet, il est très difficile d'être optimal pour le GC. Le GC en sait moins que le développeur. Ses heuristiques sont basées sur des compteurs de mémoire et des choses comme le taux de collecte, etc. Les heuristiques sont généralement bonnes, mais elles ne capturent pas les changements soudains dans le comportement des applications tels que la libération de grandes quantités de mémoire gérée. Il est également aveugle aux ressources non gérées et aux exigences de latence.

Notez que les coûts du GC varient en fonction de la taille du tas et du nombre de références sur le tas. Sur un petit tas, le coût peut être très faible. J'ai vu les taux de collecte G2 avec .NET 4.5 de 1-2 Go/sec sur une application de production avec une taille de tas de 1 Go.

67
usr

En règle générale, un garbage collector va collecter lorsqu'il s'exécute en "pression de mémoire", et il est considéré comme une bonne idée de ne pas le collecter à d'autres moments car vous pourriez provoquer des problèmes de performances ou même des pauses notables dans l'exécution de votre programme. Et en fait, le premier point dépend du second: pour un ramasse-miettes générationnel, au moins, il s'exécute plus efficacement plus le rapport ordures/bons objets est élevé = donc afin de minimiser le quantité de temps passé à suspendre le programme, il doit tergiverser et laisser les ordures s'accumuler autant que possible.

Le moment approprié pour appeler manuellement le garbage collector est alors lorsque vous avez terminé de faire quelque chose qui 1) est susceptible d'avoir créé beaucoup de déchets et 2) que l'utilisateur attend un certain temps et laisse le système ne répond pas en tous cas. Un exemple classique est à la fin du chargement de quelque chose de grand (un document, un modèle, un nouveau niveau, etc.)

27
Mason Wheeler

Une chose que personne n'a mentionnée est que, alors que le GC Windows est incroyablement bon, le GC sur Xbox est une ordure (jeu de mots voulu).

Ainsi, lors du codage d'un jeu XNA destiné à fonctionner sur XBox, il est absolument crucial de synchroniser la collecte des ordures avec les moments opportuns, ou vous aurez d'horribles hoquets FPS intermittents. De plus, sur XBox, il est courant d'utiliser la méthode structs, bien plus souvent que vous ne le feriez normalement, pour minimiser le nombre d'objets qui doivent être récupérés.

La collecte des ordures est avant tout un outil de gestion de la mémoire. En tant que tels, les ramasseurs d'ordures collecteront en cas de pression de mémoire.

Les poubelles modernes sont très bonnes et s'améliorent, il est donc peu probable que vous puissiez les améliorer en collectant manuellement. Même si vous pouvez améliorer les choses aujourd'hui, il se pourrait bien qu'une future amélioration de votre ramasse-miettes choisi rende votre optimisation inefficace, voire contre-productive.

Cependant, les récupérateurs de place n'essaient généralement pas d'optimiser l'utilisation des ressources autres que la mémoire. Dans les environnements de récupération de place, la plupart des ressources non-mémoire précieuses ont une méthode close ou similaire, mais il y a des occasions où ce n'est pas le cas pour une raison quelconque, comme la compatibilité avec une API existante.

Dans ces cas, il peut être judicieux d'appeler manuellement la récupération de place lorsque vous savez qu'une ressource non mémoire précieuse est utilisée.

RMI

Un exemple concret de ceci est l'invocation de méthode à distance de Java. RMI est une bibliothèque d'appels de procédure distante. Vous disposez généralement d'un serveur, qui met divers objets à la disposition des clients. Si un serveur sait qu'un objet n'est utilisé par aucun client, alors cet objet est éligible pour le garbage collection.

Cependant, le seul moyen pour le serveur de le savoir est que le client le lui dise et que le client indique au serveur qu'il n'a plus besoin d'un objet une fois que le client a récupéré les ordures quoi qu'il l'utilise.

Cela pose un problème, car le client peut avoir beaucoup de mémoire libre, donc peut ne pas exécuter la récupération de place très fréquemment. Pendant ce temps, le serveur peut avoir beaucoup d'objets inutilisés en mémoire, qu'il ne peut pas collecter car il ne sait pas que le client ne les utilise pas.

La solution dans RMI est que le client exécute périodiquement le ramasse-miettes, même lorsqu'il dispose de beaucoup de mémoire, pour garantir que les objets sont collectés rapidement sur le serveur.

4
James_pic

Un exemple concret:

J'avais une application Web qui utilisait un très grand ensemble de données qui changeaient rarement et qui devaient être accédées très rapidement (assez rapide pour une réponse par touche via AJAX).

La chose la plus évidente à faire ici est de charger le graphique correspondant dans la mémoire et d'y accéder à partir de là plutôt que de la base de données, en mettant à jour le graphique lorsque la base de données change.

Mais étant très important, une charge naïve aurait pris au moins 6 Go de mémoire avec les données qui devraient croître à l'avenir. (Je n'ai pas de chiffres exacts, une fois qu'il était clair que ma machine de 2 Go essayait de faire face à au moins 6 Go, j'avais toutes les mesures dont j'avais besoin pour savoir que ça n'allait pas fonctionner).

Heureusement, il y avait un grand nombre d'objets immuables à la glace dans cet ensemble de données qui étaient identiques les uns aux autres; une fois que j'avais déterminé qu'un certain lot était le même qu'un autre lot, je pouvais alias une référence à l'autre permettant de collecter de nombreuses données et donc de tout ranger dans moins d'un demi-concert.

Très bien, mais pour cela, il s'agit toujours de plus de 6 Go d'objets en l'espace d'environ une demi-minute pour arriver à cet état. Livré à lui-même, GC n'a pas fait face; le pic d'activité sur le schéma habituel de l'application (beaucoup moins lourd sur les désallocations par seconde) était trop marqué.

Donc, appeler périodiquement GC.Collect() pendant ce processus de construction signifiait que tout fonctionnait correctement. Bien sûr, je n'ai pas appelé manuellement GC.Collect() le reste du temps où l'application s'exécute.

Ce cas réel est un bon exemple des directives sur le moment où nous devrions utiliser GC.Collect():

  1. À utiliser avec un cas relativement rare de lots d'objets mis à disposition pour la collecte (une valeur de mégaoctets était mise à disposition, et cette construction graphique était un cas très rare pendant la durée de vie de l'application (environ une minute par semaine).
  2. Faites-le quand une perte de performances est relativement tolérable; cela ne s'est produit qu'au démarrage de l'application. (Un autre bon exemple de cette règle est entre les niveaux pendant une partie, ou d'autres points dans une partie où les joueurs ne seront pas contrariés par une petite pause).
  3. Profil pour être sûr qu'il y a vraiment une amélioration. (Assez facile; "ça marche" bat presque toujours "ça ne marche pas").

La plupart du temps, quand j'ai pensé que je pourrais avoir un cas où GC.Collect() mérite d'être appelé, parce que les points 1 et 2 s'appliquent, le point 3 a suggéré que cela aggravait les choses ou du moins ne rendait pas les choses meilleures (et avec peu ou pas d'amélioration, je pencherais pour ne pas appeler par appel car l'approche est plus susceptible de se révéler meilleure au cours de la durée de vie d'une application).

2
Jon Hanna

Il y a plusieurs cas où vous voudrez peut-être appeler vous-même gc ().

  • [ Certaines personnes disent que ce n'est pas bon car cela peut promouvoir des objets vers un espace de génération plus ancienne, ce qui, je le reconnais, n'est pas une bonne chose. Cependant, il est PAS toujours vrai qu'il y aura toujours des objets qui peuvent être promus. Il est certainement possible qu'après cet appel à gc(), très peu d'objets restent encore moins déplacés vers un espace de génération plus ancienne ] Lorsque vous allez créer une grande collection d'objets et utiliser beaucoup de mémoire. Vous voulez simplement dégager autant d'espace que de préparation possible. Ceci est juste du bon sens. En appelant gc() manuellement, il n'y aura pas de vérification redondante du graphique de référence sur une partie de cette grande collection d'objets que vous chargez en mémoire. En bref, si vous exécutez gc() avant de charger beaucoup en mémoire, la gc() induite pendant le chargement se produit moins au moins une fois lorsque le chargement commence à créer une pression mémoire.
  • Lorsque vous avez terminé de charger une grande collection de gros objets et vous chargerez probablement plus d'objets en mémoire. En bref, vous passez de la phase de création à la phase d'utilisation. En appelant gc() selon l'implémentation, la mémoire utilisée sera compactée, ce qui améliore considérablement la localisation du cache. Cela se traduira par une amélioration massive des performances que vous n'obtiendrez pas du profilage.
  • Similaire à la première, mais si vous faites gc() et que l'implémentation de la gestion de la mémoire le prend en charge, vous créerez une bien meilleure continuité pour votre mémoire physique. Cela rend à nouveau la nouvelle grande collection d'objets plus continue et compacte, ce qui améliore les performances
2
InformedA

La meilleure pratique est de ne pas forcer un ramasse-miettes dans la plupart des cas. (Chaque système sur lequel j'ai travaillé qui avait des ramasse-miettes forcés avait des problèmes de soulignement qui si résolu aurait supprimé la nécessité de forcer la collecte des ordures et accéléré considérablement le système.)

Il y a quelques cas vous en savez plus sur l'utilisation de la mémoire que le ramasse-miettes le fait. Il est peu probable que cela soit vrai dans une application multi-utilisateurs ou un service qui répond à plus d'une demande à la fois.

Cependant, dans certains traitements de type batch , vous en savez plus que le GC. Par exemple. envisager une application qui.

  • Reçoit une liste de noms de fichiers sur la ligne de commande
  • Traite un seul fichier puis écrit le résultat dans un fichier de résultats.
  • Pendant le traitement du fichier, crée un grand nombre d'objets interconnectés qui ne peuvent pas être collectés avant la fin du traitement du fichier (par exemple, un arbre d'analyse)
  • Ne conserve pas l'état de correspondance entre les fichiers qu'il a traités .

Vous peut-être être en mesure de faire un cas (après un examen minutieux) que vous devez forcer une récupération de place complète après avoir traité chaque fichier.

Un autre cas est un service qui se réveille toutes les quelques minutes pour traiter certains éléments et ne conserve aucun état pendant son sommeil . Forcer ensuite une collection complète juste avant d'aller dormir mai en vaut la peine.

La seule fois où j'envisagerais de forcer une collection, c'est quand je sais que beaucoup d'objets ont été créés récemment et très peu d'objets sont actuellement référencés.

Je préférerais avoir une API de récupération de place quand je pourrais lui donner des conseils sur ce type de chose sans avoir à forcer moi-même un GC.

Voir aussi " Tidbits de performance de Rico Mariani "

2
Ian

J'ai une utilisation pour l'élimination des déchets qui est quelque peu peu orthodoxe.

Il y a cette pratique erronée qui est malheureusement très répandue dans le monde C #, d'implémenter l'élimination des objets en utilisant l'idiome laid, maladroit, inélégant et sujet aux erreurs connu sous le nom de IDisposable-disposing . MSDN le décrit longuement , et beaucoup de gens ne jurent que par lui, le suivent religieusement, passent des heures et des heures à discuter précisément comment cela doit être fait, etc.

(Veuillez noter que ce que j'appelle laid ici est pas le modèle d'élimination d'objet lui-même; ce que j'appelle laid est l'idiome IDisposable.Dispose( bool disposing ) particulier.)

Cet idiome a été inventé car il est supposé impossible de garantir que le destructeur de vos objets sera toujours invoqué par le garbage collector pour nettoyer les ressources, afin que les gens effectuent le nettoyage des ressources dans IDisposable.Dispose(), et au cas où ils oublieraient, ils donnez-lui également un essai de plus depuis le destructeur. Vous savez, juste au cas où.

Mais alors, votre IDisposable.Dispose() peut avoir à la fois des objets gérés et non gérés à nettoyer, mais ceux gérés ne peuvent pas être nettoyés lorsque IDisposable.Dispose() est appelé depuis le destructeur, car ils ont déjà été pris prise en charge par le garbage collector à ce moment-là, il y a donc ce besoin d'une méthode Dispose() distincte qui accepte un indicateur bool disposing pour savoir si les objets gérés et non gérés doivent être nettoyés , ou seulement non gérés.

Excusez-moi, mais c'est juste fou.

Je vais par l'axiome d'Einstein, qui dit que les choses devraient être aussi simples que possible, mais pas plus simples. De toute évidence, nous ne pouvons pas omettre le nettoyage des ressources, donc la solution la plus simple possible doit inclure au moins cela. La prochaine solution la plus simple consiste à toujours tout éliminer au moment précis où il est censé l'être, sans compliquer les choses en s'appuyant sur le destructeur comme alternative de repli.

Maintenant, à proprement parler, il est bien sûr impossible de garantir qu'aucun programmeur ne commettra jamais l'erreur d'oublier d'invoquer IDisposable.Dispose(), mais ce que nous pouvons faire, c'est utiliser le destructeur pour attraper cette erreur. C'est très simple, vraiment: tout ce que le destructeur a à faire est de générer une entrée de journal s'il détecte que le drapeau disposed de l'objet jetable n'a jamais été défini sur true. Ainsi, l'utilisation du destructeur ne fait pas partie intégrante de notre stratégie d'élimination, mais c'est notre mécanisme d'assurance qualité. Et comme il s'agit d'un test en mode débogage uniquement, nous pouvons placer l'intégralité de notre destructeur dans un bloc #if DEBUG, Afin de ne jamais encourir de pénalité de destruction dans un environnement de production. (L'idiome IDisposable.Dispose( bool disposing ) prescrit que GC.SuppressFinalize() doit être invoqué précisément afin de réduire les frais généraux de finalisation, mais avec mon mécanisme, il est possible d'éviter complètement ces frais généraux sur l'environnement de production.)

Cela revient à l'éternel erreur dure vs erreur douce argument: l'argument IDisposable.Dispose( bool disposing ) idiom est une approche par erreur douce et représente une tentative pour permettre au programmeur d'oublier d'appeler Dispose() sans que le système ne tombe en panne, si possible. L'approche par erreur matérielle indique que le programmeur doit toujours s'assurer que Dispose() sera invoqué. La pénalité généralement prescrite par l'approche des erreurs matérielles dans la plupart des cas est l'échec de l'assertion, mais pour ce cas particulier, nous faisons une exception et réduisons la pénalité à une simple émission d'une entrée du journal des erreurs.

Donc, pour que ce mécanisme fonctionne, la version DEBUG de notre application doit effectuer une élimination complète des déchets avant de quitter, afin de garantir que tous les destructeurs seront invoqués, et ainsi attraper tous les objets IDisposable que nous avons oubliés disposer.

0
Mike Nakis

Pouvez-vous me dire dans quel genre de scénario c'est en fait une bonne ou une bonne idée de forcer la collecte des ordures? Je ne demande pas de cas spécifiques C # mais plutôt tous les langages de programmation qui ont un garbage collector. Je sais que vous ne pouvez pas forcer GC sur tous les langages, comme Java, mais supposons que vous le puissiez.

Le fait de parler de façon très théorique et de ne pas tenir compte des problèmes tels que certaines implémentations du GC ralentissant les choses au cours de leurs cycles de collecte, le plus gros scénario auquel je peux penser pour forcer la collecte des ordures est un logiciel essentiel à la mission où les fuites logiques sont préférables à des suspensions de pointeurs pendantes, par exemple, en raison d'un crash à des moments inattendus pourrait coûter des vies humaines ou quelque chose de ce genre.

Si vous regardez certains des jeux indépendants plus timides écrits en utilisant des langages GC comme les jeux Flash, ils fuient comme des fous mais ils ne plantent pas. Ils pourraient prendre dix fois plus de mémoire 20 minutes pour jouer au jeu car une partie de la base de code du jeu a oublié de définir une référence sur null ou de la supprimer d'une liste, et les fréquences d'images peuvent commencer à souffrir, mais le jeu fonctionne toujours. Un jeu similaire écrit en utilisant un codage C ou C++ de mauvaise qualité pourrait se bloquer en raison de l'accès à des pointeurs pendants à la suite du même type d'erreur de gestion des ressources, mais il ne fuirait pas autant.

Pour les jeux, le crash peut être préférable dans le sens où il peut être rapidement détecté et corrigé, mais pour un programme critique, un crash à des moments totalement inattendus peut tuer quelqu'un. Donc, les principaux cas, je pense, seraient des scénarios où la non-panne ou d'autres formes de sécurité sont absolument critiques, et une fuite logique est une chose relativement triviale en comparaison.

Le scénario principal où je pense qu'il est mauvais de forcer GC est pour des choses où la fuite logique est en fait moins préférable qu'un crash. Avec les jeux, par exemple, le crash ne tuera pas nécessairement quelqu'un et il pourrait être facilement détecté et corrigé pendant les tests internes, tandis qu'une fuite logique pourrait passer inaperçue même après la livraison du produit, sauf si elle est si grave qu'elle rend le jeu injouable en quelques minutes. . Dans certains domaines, un plantage facilement reproductible qui se produit lors des tests est parfois préférable à une fuite que personne ne remarque immédiatement.

Un autre cas auquel je peux penser où il pourrait être préférable de forcer GC sur une équipe est pour un programme de très courte durée, comme juste quelque chose exécuté à partir de la ligne de commande qui effectue une tâche puis s'arrête. Dans ce cas, la durée de vie du programme est trop courte pour rendre toute fuite logique non triviale. Les fuites logiques, même pour les grandes ressources, ne deviennent généralement problématiques que des heures ou des minutes après avoir exécuté le logiciel, il est donc peu probable qu'un logiciel destiné à être exécuté pendant 3 secondes ne rencontre jamais de problèmes de fuites logiques, et cela pourrait faire beaucoup de choses plus simple d'écrire de tels programmes de courte durée si l'équipe vient d'utiliser GC.

0
user204677