Quand est-ce une bonne idée de forcer le ramassage des ordures?


135

Je lisais donc une question sur le fait de forcer le ramasse-miettes C # à s'exécuter là où presque toutes les réponses sont identiques: vous pouvez le faire, mais vous ne devriez pas, sauf dans de très rares cas . Malheureusement, personne n’explique ce que sont de tels cas.

Pouvez-vous me dire dans quel type de scénario il est en fait une bonne ou raisonnable idée de forcer le ramassage des ordures?

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


17
"mais plutôt, tous les langages de programmation qui ont un ramasse-miettes" Différentes langues (ou, plus exactement, différentes implémentations ) utilisent différentes méthodes pour la ramasse-miettes, il est donc peu probable que vous trouviez une règle universelle.
colonel Trente-deux

4
@Doval Si vous êtes confronté à une contrainte de temps réel et que le GC ne fournit pas de garanties correspondantes, vous êtes entre le marteau et l'enclume. Cela pourrait réduire les pauses indésirables par rapport à ne rien faire, mais de ce que j'ai entendu dire, il est "plus facile" d'éviter d'allouer dans le cours normal des opérations.

3
J'avais l'impression que si vous vous attendiez à avoir des délais très difficiles en temps réel, vous n'utiliseriez jamais un langage GC.
GregRos

4
Je ne vois pas comment vous pouvez répondre à cela d'une manière non spécifique à la VM. Pertinent pour les processus 32 bits, sans pertinence pour les processus 64 bits. JVM .NET et pour le haut de gamme
rwong

3
@ DavidConrad, vous pouvez le forcer en C #. D'où la question.
Omega

Réponses:


127

Vous ne pouvez vraiment pas faire de déclarations générales sur la manière appropriée d'utiliser toutes les implémentations du GC. Ils varient énormément. Je parlerai donc de celui .NET auquel vous avez fait référence à 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 le suivant: ne le faites jamais.

Si vous connaissez vraiment les détails complexes du GC, vous n'aurez pas besoin de mes conseils, cela n'aura donc aucune importance. Si vous ne 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 voir les détails du fonctionnement du GC. à l'intérieur et à l'extérieur, et alors seulement, vous saurez la réponse .

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

GC.Collect est une API disponible que vous pouvez utiliser pour définir le minutage des événements. Vous pouvez profiler un algorithme, collecter et profiler un autre algorithme immédiatement après, sachant que la GC du premier algorithme ne se produisait pas pendant le second altérant les résultats.

Ce type de profilage est la seule fois que je suggère de collecter manuellement à qui que ce soit.


Exemple contrarié quand même

Un cas d'utilisation possible est que, si vous chargez des objets très volumineux, ils se retrouveront dans le tas d'objets volumineux qui ira directement à la génération 2, bien que la génération 2 soit destinée aux objets à longue durée de vie car elle collecte moins fréquemment. Si vous savez que vous chargez des objets éphémères dans la génération 2 pour une raison quelconque, vous pouvez les nettoyer plus rapidement pour que votre génération 2 reste plus petite et que ses collections soient plus rapides.

C’est le meilleur exemple que je puisse donner, et ce n’est pas bon - la pression de la loi sur la santé que vous établissez ici provoquerait des collectes plus fréquentes, et les collections sont si fréquentes qu’il en est - il est probable que cela effacerait la mémoire au même moment. vite que vous le souffliez avec des objets temporaires. Simplement , je ne fais pas confiance moi - même de présumer une meilleure fréquence de collecte que le GC lui - même - à l' écoute par des gens bien loin plus intelligents que moi


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

Tout ce que je pense savoir sur le .NET GC

S'il vous plaît, toute personne qui trouve des erreurs ici - corrigez-moi. La plupart des membres de GC sont connus pour être de la magie noire et bien que j'essaie de laisser de côté des détails dont je n'étais pas sûr, je me trompe probablement encore.

Vous trouverez ci-dessous de nombreux détails dont je ne suis pas certain, ainsi que des informations beaucoup plus vastes que je ne connais pas. Utilisez ces informations à vos risques et périls.


Concepts GC

Le .NET GC se produit à des moments incohérents. C'est pourquoi on l'appelle "non déterministe". Cela signifie que vous ne pouvez pas vous fier à cela à des moments spécifiques. Il s'agit également d'un ramasse-miettes générationnel, ce qui signifie qu'il partitionne vos objets en fonction du nombre de passes GC qu'ils ont effectuées.

Les objets de la génération 0 ont vécu 0 collections. Celles-ci ont été récemment créées. Aucune collection ne s’est produite depuis leur instanciation. Les objets de votre segment de génération 1 ont traversé une passe de collecte, tout comme les objets de votre segment de génération 2 ont traversé 2 passes de collection.

Maintenant, il convient de noter la raison pour laquelle il qualifie ces générations et partitions en conséquence. Le .NET GC ne reconnaît que ces trois générations, car les passes de collection qui vont sur ces trois tas sont légèrement différentes. Certains objets peuvent survivre des passages de collection des milliers de fois. Le GC ne fait que les laisser de l’autre côté de la partition de pile de la génération 2, il est inutile de les partitionner ailleurs car il s’agit en fait de la génération 44; la collection transmise est la même chose que tout le tas de la 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.


Que contient une collection

Le concept de base d'une passe de collection GC consiste à vérifier 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, de sorte qu'il ne peut pas être supprimé. Toutefois, si aucune racine GC n'est trouvée pour un objet, cela signifie que le processus en cours n'a plus besoin de cet objet. Il peut donc le supprimer pour libérer de la mémoire pour de nouveaux objets.

Maintenant, après avoir fini de nettoyer un tas d'objets et d'en laisser quelques-uns, il y aura un effet secondaire malheureux: des espaces libres entre les objets vivants, là où les objets morts ont été enlevés. Cette fragmentation de la mémoire, si elle est laissée à elle-même, gaspille simplement de la mémoire. Par conséquent, les collections effectuent ce que l'on appelle "compactage": elles prennent tous les objets en direct et les compressent dans le segment de mémoire de sorte que la mémoire disponible soit contiguë d'un côté du segment de mémoire 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 effectuées, voyons pourquoi ces partitions existent.


Collection Gen 0

La génération 0 étant les objets les plus récents, elle a tendance à être très petite - vous pouvez donc la collecter en toute sécurité très fréquemment . La fréquence garantit que le segment de mémoire reste petit et les collections sont très rapides car elles sont collectées sur un aussi petit segment. Ceci est basé plus ou moins sur une heuristique affirmant que: Une grande majorité des objets temporaires que vous créez sont très temporaires, ils ne sont donc plus utilisés ou référencés presque immédiatement après leur utilisation, et peuvent donc être collectés.


Collection Gen 1

La génération 1 étant des objets qui ne font pas partie de cette catégorie très temporaire d'objets, leur durée de vie peut encore être assez courte, car une grande partie des objets créés ne sont pas utilisés longtemps. Par conséquent, la génération 1 collecte également assez fréquemment, encore une fois, la taille de son paquet est réduite afin que ses collections soient rapides. Cependant, l’hypothèse est que moins d’objets sont temporaires que la génération 0, donc elle collecte moins souvent que la génération 0

Je dirai franchement que je ne connais pas les mécanismes techniques qui diffèrent entre le laissez-passer de collection du groupe 0 et celui du groupe 1, s’il en existe un autre 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à où vivent tous vos objets permanents - l’objet Main()dans lequel vous vivez, par exemple, et tout ce qui est Main()référencé car ils seront enracinés jusqu’à votre Main()retour à la fin de votre processus.

Étant donné que la génération 2 est un seau pour pratiquement tout ce que les autres générations ne pourraient pas collecter, ses objets sont en grande partie permanents, ou vivent au moins longtemps. Donc, sachant que très peu de ce qui est dans la génération 2 sera en réalité une chose qui peut être collectée, il n’a pas besoin de la collecter fréquemment. Cela permet également à sa collection d'être plus lente, car elle s'exécute beaucoup moins fréquemment. C'est donc essentiellement là où ils ont ajouté tous les comportements supplémentaires pour les scénarios impairs, car ils ont le temps de les exécuter.


Tas d'objets volumineux

Un exemple des comportements supplémentaires de la génération 2 est qu'il effectue également la collecte sur le segment d'objets volumineux. Jusqu'à présent, je parlais entièrement du segment de petits objets, mais le runtime .NET attribue des éléments de certaines tailles à un segment séparé 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 segment de petits objets. S'il y a un objet vivant de 10 Mo dans la génération 1, il faudra beaucoup plus de temps pour terminer le compactage après la collecte, ce qui ralentira la collection de la génération 1. Ainsi, cet objet de 10 Mo est affecté au segment d'objets volumineux et collecté au cours de la génération 2, qui s'exécute si rarement.


La finalisation

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

Le mécanisme utilisé par les finaliseurs est leur référence directe dans une file d'attente de finalisation. Lorsque le moteur d'exécution attribue un objet à un finaliseur, il ajoute un pointeur à cet objet dans la file d'attente de finalisation et verrouille votre objet à la place (appelé «épinglage») afin que le compactage ne le déplace pas, ce qui romprait la référence à la file d'attente de finalisation. Au fur et à mesure que les collectes se produisent, votre objet n'aura finalement plus de racine GC, mais la finalisation devra être exécutée avant de pouvoir être collectée. Ainsi, lorsque l'objet est mort, la collection déplacera sa référence de la file d'attente de finalisation et y placera une référence sur ce que l'on appelle la file d'attente "FReachable". Ensuite, la collection continue. À un autre moment "non déterministe" de l'avenir, un thread séparé, appelé thread de finaliseur, passera par la file d'attente FReachable, exécutant les finaliseurs pour chacun des objets référencés. Une fois terminé, la file d'attente FReachable est vide et il a basculé un bit sur l'en-tête de chaque objet indiquant qu'il n'est pas nécessaire de le finaliser (ce bit peut également être retourné manuellement avecGC.SuppressFinalizece qui est courant dans les Dispose()méthodes), je soupçonne également que cela a désépinglé les objets, mais ne me citez pas là-dessus. La prochaine collection qui arrive sur le tas dans lequel se trouve cet objet le récupérera finalement. Les collections de la génération 0 ne font même pas attention aux objets avec ce bit de finalisation nécessaire, elles sont automatiquement promues, sans même vérifier leur racine. Un objet non raciné nécessitant une finalisation dans la génération 1 sera jeté dans la FReachablefile d'attente, mais la collection ne fait rien d'autre avec elle. Elle vit donc dans la génération 2. Ainsi, tous les objets dotés d'un finaliseur ne GC.SuppressFinalizeseront collectés dans Gen 2.


4
@ FlorianMargaine ouais ... dire n'importe quoi à propos de "GC" à travers toutes les implémentations n'a vraiment aucun sens ..
Jimmy Hoffa

10
tl; dr: utilisez plutôt des pools d'objets.
Robert Harvey

5
tl; dr: Pour le chronométrage / profilage, cela peut être utile.
kutschkem

3
@ Den après avoir lu ma description ci-dessus de la mécanique (telle que je la comprends), quel serait l'avantage à votre avis? Vous nettoyez un grand nombre d'objets - dans le SOH (ou LOH?)? Avez-vous simplement provoqué une pause des discussions pour cette collection? Cette collection a-t-elle simplement promu deux fois plus d'objets à la génération 2 que ce qu'elle a été nettoyée? La collection at-elle provoqué un compactage sur LOH (l’avez-vous activée?)? Combien de GCs avez-vous et votre GC est-il en mode serveur ou bureau? GC est un iceberg feckin ', la trahison est sous les eaux. Soyons juste clair. Je ne suis pas assez intelligent pour collecter confortablement.
Jimmy Hoffa

4
@ RobertHarvey Les pools d'objets ne sont pas non plus une solution miracle. La génération 0 du ramasse-miettes est déjà effectivement un pool d'objets. Elle est généralement dimensionnée pour s'adapter au plus petit niveau de cache. Ainsi, les nouveaux objets sont généralement créés dans la mémoire déjà en cache. Votre pool d'objets est maintenant en concurrence avec la pépinière du GC pour obtenir le cache, et si la somme de la pépinière du GC et de votre pool est plus grande que le cache, il est évident que vous manquerez de cache. Et si vous envisagez d'utiliser le parallélisme maintenant, vous devez réimplémenter la synchronisation et vous soucier du faux partage.
Doval

68

Malheureusement, personne n’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 provient de mon expérience avec la littérature .NET et GC. Il devrait bien se généraliser à d'autres plates-formes (du moins celles qui ont un GC significatif).

  • Repères de toutes sortes. Vous voulez un état de segment géré géré connu au début d'un test afin que le CPG ne se déclenche pas de manière aléatoire pendant les tests. Lorsque vous répétez un repère, vous voulez le même nombre et la même quantité de travail de GC dans chaque répétition.
  • Libération soudaine de ressources. Par exemple, fermer une fenêtre graphique significative ou actualiser un cache (et libérer ainsi l'ancien contenu potentiellement volumineux). Le CPG ne peut pas détecter cela, car vous ne définissez qu'une référence à null. Le fait que cet orphelin orpheline d'un graphe d'objet entier ne soit pas facilement détectable.
  • Libération des ressources non gérées qui ont fui . Cela ne devrait jamais arriver, bien sûr, mais j'ai déjà vu des cas où une bibliothèque tierce avait des fuites (comme des objets COM). Le développeur était forcé d'induire parfois une collection.
  • Applications interactives telles que les jeux . Pendant le jeu, les budgets de jeu par image sont très stricts (60Hz => 16ms par image). Afin d'éviter les hickups, vous avez besoin d'une stratégie pour gérer les GC. Une de ces stratégies consiste à retarder le plus possible les CPG G2 et à les forcer à un moment opportun, comme un écran de chargement ou une scène cinématique. Le GC ne peut pas savoir quand se situe 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 lors de la commutation de la rotation de l'équilibreur de charge. De cette manière, la latence G2 n’est jamais signalée à l’utilisateur.

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

Les exemples donnés ci-dessus visent la cohérence et la 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 répandue selon laquelle le GC est une entité divine qui induit une collection chaque fois que cela est en effet optimal. Je ne connais pas de GC 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 du 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 de GC varient en fonction de la taille du segment et du nombre de références sur le segment. Sur un petit tas, le coût peut être très faible. J'ai vu des taux de collecte G2 avec .NET 4.5 de 1 à 2 Go / s sur une application de production de 1 Go.


Pour le cas du contrôle de latence, je suppose qu'au lieu de le faire périodiquement, vous pouvez également le faire par besoin (c'est-à-dire lorsque l'utilisation de la mémoire dépasse un certain seuil).
Paŭlo Ebermann

3
+1 pour l'avant-dernier paragraphe. Certaines personnes ont le même sentiment vis-à-vis des compilateurs et n'hésitent pas à appeler n'importe quoi "une optimisation prématurée". Je leur dis habituellement quelque chose de similaire.
Honza Brabec

2
+1 pour ce paragraphe également. Je trouve choquant que les gens pensent qu'un programme informatique écrit par quelqu'un d'autre doit nécessairement comprendre les caractéristiques de performance de leur programme mieux qu'eux-mêmes.
Mehrdad

1
@HonzaBrabec Le problème est le même dans les deux cas: si vous pensez en savoir plus que le GC ou le compilateur, il est très facile de vous blesser. Si vous en savez plus, vous n'optimisez que lorsque vous savez que ce n'est pas prématuré.
svick

27

En règle générale, un ramasse-miettes va collecter lorsqu'il est soumis à une "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 causer des problèmes de performances ou même des pauses sensibles 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 fonctionne plus efficacement lorsque le rapport entre les ordures et les objets de qualité augmente. Par conséquent , afin de minimiser le temps passé en pause du programme. , il doit tergiverser et laisser les ordures s’empiler le plus possible.

Le moment approprié pour appeler manuellement le ramasse-miettes est alors, une fois que vous avez fini de faire quelque chose qui 1) est susceptible d'avoir créé beaucoup de poubelle et 2) que l'utilisateur s'attend à prendre un certain temps et à laisser le système inactif. en tous cas. Un exemple classique est à la fin du chargement d’un élément volumineux (un document, un modèle, un nouveau niveau, etc.)


12

Une chose que personne n’a mentionnée, c’est que, même si le CPG Windows est incroyablement bon, le CPG sur Xbox est un déchet (jeu de mots) .

Ainsi, lorsque vous codez un jeu XNA destiné à être exécuté sur une XBox, il est absolument essentiel de minuter la récupération des ordures au meilleur moment, sinon vous aurez un horrible hoquet intermittent du FPS. En outre, sur XBox, il est courant d’utiliser le structmoyen le plus souvent utilisé pour minimiser le nombre d’objets devant être nettoyés.


4

Le ramassage des ordures est avant tout un outil de gestion de la mémoire. En tant que tels, les éboueurs se rassemblent lorsqu'il y a une pression de mémoire.

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

Toutefois , les garbage collectors ne tentent généralement pas d'optimiser l'utilisation de ressources autres que la mémoire. Dans les environnements dépourvus de mémoire, la plupart des ressources hors mémoire précieuses ont une closeméthode ou une méthode similaire, mais il existe des cas où ce n'est pas le cas pour une raison quelconque, telle que 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 valable 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 avez généralement un serveur, qui met divers objets à la disposition des clients. Si un serveur sait qu'un objet n'est utilisé par aucun client, cet objet est alors éligible pour le garbage collection.

Cependant, le serveur ne le sait que si le client le lui dit et qu'il ne lui dit plus qu'il n'a plus besoin d'un objet une fois que le client a récupéré n'importe quel document.

Cela pose un problème, car le client peut disposer de beaucoup de mémoire libre et ne peut donc pas exécuter une récupération de place très fréquemment. En attendant, 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.

Dans RMI, la solution consiste à ce que le client exécute une collecte de place périodiquement, même lorsqu'il dispose de beaucoup de mémoire, pour s'assurer que les objets sont collectés rapidement sur le serveur.


"Dans ces cas, il peut être judicieux d’appeler manuellement la récupération de place lorsque vous savez qu’une ressource précieuse non-mémoire est utilisée" - si une ressource non-mémoire est utilisée, vous devez utiliser un usingbloc ou appeler une Closeméthode pour s'assurer que la ressource est jetée dès que possible. Compter sur GC pour nettoyer des ressources non-mémoire n’est pas fiable et pose toutes sortes de problèmes (en particulier avec des fichiers dont l'accès doit être verrouillé et qui ne peuvent donc être ouverts qu'une seule fois).
Jules

Et comme indiqué dans la réponse, lorsqu'une closeméthode est disponible (ou que la ressource peut être utilisée avec un usingbloc), il s'agit de la bonne approche. La réponse traite spécifiquement des rares cas où ces mécanismes ne sont pas disponibles.
James_pic

Mon opinion personnelle est que toute interface qui gère une ressource non-mémoire mais ne fournit pas de méthode de fermeture est une interface qui ne doit pas être utilisée , car il est impossible de l'utiliser de manière fiable.
Jules

@Jules je suis d'accord, mais parfois c'est inévitable. Parfois, les abstractions fuient, et utiliser une abstraction qui fuit est préférable à ne pas utiliser d'abstraction. Parfois, vous devez travailler avec des codes hérités qui vous demandent de faire des promesses que vous savez que vous ne pouvez pas tenir. Oui, c'est rare et devrait être évité si possible, et il y a une raison pour laquelle il y a tous ces avertissements sur le forçage du ramassage des ordures, mais ces situations se produisent, et le PO demandait à quoi ressemblaient ces situations - auxquelles j'ai répondu .
James_pic

2

La meilleure pratique consiste à ne pas forcer une récupération de place dans la plupart des cas. (Tous les systèmes sur lesquels j'ai travaillé avaient des collectes de déchets forcées, des problèmes de soulignement qui, s'ils étaient résolus, auraient éliminé la nécessité de forcer la collecte des déchets et auraient grandement accéléré le système.)

Il existe quelques casvous en savez plus sur l'utilisation de la mémoire que le récupérateur de place en connaît. Il est peu probable que cela soit vrai dans une application multi-utilisateur ou dans un service qui répond à plusieurs demandes à la fois.

Cependant, dans certains types de traitement par lots, vous en savez plus que le CPG. Par exemple, considérons une application qui.

  • Est donné une liste de noms de fichiers sur la ligne de commande
  • Traite un fichier unique, puis écrit le résultat dans un fichier de résultats.
  • Lors du traitement du fichier, crée un grand nombre d’objets liés qui ne peuvent pas être collectés tant que le traitement du fichier n’est pas terminé (par exemple, un arbre d’analyse).
  • Ne conserve pas l'état de correspondance entre les fichiers qu'il a traités .

Vous pourrez peut- être vérifier (après avoir soigneusement vérifié) que vous devez forcer un nettoyage complet 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 une collection complète juste avant d'aller dormir peut en valoir 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 que très peu d'objets sont actuellement référencés.

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

Voir aussi " Tidbits de performance de Rico Mariani "


2

Vous pouvez appeler gc () dans plusieurs cas.

  • [ Certaines personnes disent que cela n’est pas bon, car cela peut promouvoir des objets dans l’espace des générations plus anciennes, ce qui, à mon avis, n’est pas une bonne chose. Cependant, il n'est PAS toujours vrai qu'il y aura toujours des objets pouvant être promus. Il est certainement possible qu'après cet gc()appel, très peu d'objets restent laissés à eux-mêmes et qu'ils soient déplacés dans 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 libérer autant d’espace que de préparation. Ceci est juste du bon sens. En appelant gc()manuellement, il n'y aura pas de vérification de graphe de référence redondante sur une partie de cette grande collection d'objets que vous chargez en mémoire. En bref, si vous courez gc()avant de charger beaucoup en mémoire, legc() induit pendant la charge se produit au moins une fois au moins lorsque le chargement commence à créer une pression de la mémoire.
  • Quand vous avez fini de charger une grande collection degrosobjets et vous aurez peu de chances de charger plus d’objets en mémoire. En bref, vous passez de la phase de création à la phase d'utilisation. En appelant en gc()fonction de l'implémentation, la mémoire utilisée sera compactée, ce qui améliorera considérablement l'emplacement de la mémoire cache. Cela se traduira par une amélioration massive des performances que vous ne tirerez pas du profilage .
  • Semblable au premier, mais de l'avis que si vous le faites gc()et que l'implémentation de la gestion de la mémoire prend en charge, vous créerez une bien meilleure continuité pour votre mémoire physique. Cela rend encore une fois la nouvelle grande collection d'objets plus continue et compacte, ce qui améliore les performances

1
Quelqu'un peut-il indiquer la raison du vote négatif? Moi-même, je n'en sais pas assez pour juger de la réponse (à première vue, cela me semble sensé).
Omega

1
Je suppose que vous avez un vote négatif pour le troisième point. Potentiellement aussi pour dire "Ceci est juste du bon sens".
Immibis

2
Lorsque vous créez une grande collection d'objets, le CPG doit être suffisamment intelligent pour savoir si une collection est nécessaire. Idem lorsque la mémoire doit être compactée. Compter sur le GC pour optimiser la mémoire des objets liés ne semble pas réaliste. Je pense que vous pouvez trouver d'autres solutions (struct, unsafe, ...). (Je ne suis pas le votant).
Guillaume

3
À mon avis, votre première idée d’un bon moment est tout simplement un mauvais conseil. Les chances sont élevées qu'il ya eu une collection récemment afin que votre tentative de collecte de nouveau simplement va promouvoir arbitrairement des objets à des générations plus tard, ce qui est presque toujours mauvais. Les générations suivantes ont des collections qui prennent plus de temps à commencer, augmenter la taille de leur tas «pour libérer autant d’espace que possible» rend la situation plus problématique. De plus, si vous êtes sur le point d'augmenter la pression de la mémoire avec une charge, vous risquez de commencer à induire des collections de toute façon, ce qui fonctionnera plus lentement à cause de l'augmentation de la génération 1/2
Jimmy Hoffa

2
By calling gc() depending on implementation, the memory in used will be compacted which massively improves cache locality. This will result in massive improve in performance that you will not get from profiling.Si vous allouez une tonne d'objets à la suite, il y a de fortes chances qu'ils soient déjà compactés. Si quelque chose, la collecte des ordures peut les déplacer légèrement. Quoi qu'il en soit, l'utilisation de structures de données denses et non aléatoires en mémoire aura un impact plus important. Si vous utilisez une liste chaînée naïve composée d'un élément par nœud, aucune tromperie manuelle du GC ne compensera cela.
Doval

2

Un exemple concret:

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

Ici, il est assez évident de charger le graphique correspondant dans la mémoire et d’y accéder depuis la base de données plutôt que dans la base de données, en mettant à jour le graphique lorsque la base de données est modifiée.

Mais, étant très volumineux, une charge naïve aurait nécessité au moins 6 Go de mémoire, car les données allaient s’accroître à l’avenir. (Je n'ai pas les chiffres exacts, une fois qu'il était clair que mon ordinateur de 2 Go essayait de gérer au moins 6 Go, j'avais toutes les mesures nécessaires pour savoir que cela ne marcherait pas).

Heureusement, cet ensemble de données contenait un grand nombre d'objets immuables dans un popsicle identiques; une fois que j'avais déterminé qu'un lot était identique à un autre, je pouvais faire référence à une référence à une autre, ce qui permettait de collecter une grande quantité de données et donc de tout ranger dans moins d'un demi-pouce.

Tout va bien, mais pour cela, il reste encore plus de 6 Go d'objets en l'espace d'une demi-minute pour arriver à cet état. Laissé à lui-même, GC n'a pas fait face; le pic d'activité par rapport au schéma habituel de l'application (beaucoup moins de désallocations par seconde) était trop marqué.

Donc appeler périodiquement GC.Collect()pendant ce processus de construction signifiait que tout fonctionnait sans heurts. Bien sûr, je n'ai pas appelé manuellement GC.Collect()le reste du temps pendant lequel l'application est exécutée.

Ce cas concret est un bon exemple des directives concernant le moment où nous devrions utiliser GC.Collect():

  1. Utilisation avec un cas relativement rare de nombreux objets mis à disposition pour la collecte (une valeur de mégaoctets était disponible, et cette création de graphique était un cas très rare sur la durée de vie de l'application (environ une minute par semaine).
  2. Faites-le quand une perte de performance est relativement tolérable; cela ne s'est produit qu'au démarrage de l'application. (Un autre bon exemple de cette règle concerne les niveaux d'un jeu ou d'autres points d'un jeu où les joueurs ne seront pas dérangés par une 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, lorsque je pensais avoir un cas où il GC.Collect()valait la peine d'être appelé, parce que les points 1 et 2 s'appliquaient, le point 3 suggérait que cela aggraverait les choses ou du moins ne rendrait pas les choses meilleures (et avec peu ou pas d'amélioration penchez-vous plutôt pour ne pas appeler sur appelant comme approche plus susceptible de se révéler meilleure au cours de la durée de vie d’une application).


0

J'ai un usage pour l'élimination des déchets qui est un peu peu orthodoxe.

Il existe cette pratique malavisée qui est malheureusement très répandue dans le monde C #, consistant à mettre au rebut des objets en utilisant l’idiome laid, maladroit, inélégant et sujet aux erreurs, connu sous le nom de «se débarrasser d’Isposable» . MSDN le décrit en détail , et beaucoup de gens jurent, le suivent religieusement, passent des heures et des heures à discuter précisément de la marche à suivre, etc.

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

Cet idiome a été inventé car il est censé être impossible de garantir que le destructeur de vos objets sera toujours appelé par le ramasse-miettes pour nettoyer les ressources. Ainsi, les utilisateurs effectuent un nettoyage des ressources à l'intérieur IDisposable.Dispose(). S'ils l'oublient, ils le testent également dans le destructeur. Vous savez, juste au cas où.

Mais alors vos IDisposable.Dispose()objets à nettoyer peuvent être gérés, mais non gérés, mais les objets gérés ne peuvent pas être nettoyés quand ils IDisposable.Dispose()sont appelés à partir du destructeur, car ils ont déjà été pris en charge par le ramasse-miettes à ce moment-là. Cette Dispose()méthode nécessite-t-elle une méthode distincte qui accepte un bool disposingindicateur pour savoir si les objets gérés et non gérés doivent être nettoyés, ou uniquement les objets non gérés?

Excusez-moi, mais c'est juste fou.

Je me fie à l'axiome d'Einstein, qui dit que les choses devraient être aussi simples que possible, mais pas plus simples. Il est clair que nous ne pouvons pas omettre le nettoyage des ressources, la solution la plus simple possible doit donc inclure au moins cela. La solution la plus simple suivante consiste à toujours disposer de tout au moment précis où elle est censée être éliminée, sans compliquer les choses en faisant confiance au destructeur comme solution de rechange.

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 intercepter cette erreur. C'est très simple, vraiment: tout ce que le destructeur doit faire est de générer une entrée de journal s'il détecte que l' disposedindicateur de l'objet jetable n'a jamais été défini 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’ensemble de notre destructeur à l’intérieur d’un #if DEBUGbloc afin de ne jamais encourir de pénalité de destruction dans un environnement de production. (L' IDisposable.Dispose( bool disposing )idiome prescrit queGC.SuppressFinalize() doit être invoqué précisément pour réduire les frais généraux de la 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' argument éternel erreur dure vs erreur logicielle: l' IDisposable.Dispose( bool disposing )idiome est une approche d'erreur logicielle, et représente une tentative pour permettre au programmeur d'oublier l'invocation Dispose()sans que le système échoue, si possible. L'approche de l'erreur dure dit que le programmeur doit toujours s'assurer que Dispose()cela sera invoqué. La pénalité généralement prescrite par la méthode de la grosse erreur est dans la plupart des cas l’échec de l’assertion, mais dans 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 capturera ainsi tous les IDisposableobjets que nous avons oublié de supprimer.


Now, strictly speaking, it is of course impossible to guarantee that no programmer will ever make the mistake of forgetting to invoke IDisposable.Dispose()En fait, ce n’est pas le cas, bien que je ne pense pas que C # en soit capable. Ne pas exposer la ressource; à la place, fournissez un DSL pour décrire tout ce que vous ferez avec elle (en gros, un monade), ainsi qu'une fonction qui acquiert la ressource, fait les choses, la libère et renvoie le résultat. L'astuce consiste à utiliser le système de typographie pour garantir que, si quelqu'un passe en contrebande une référence à la ressource, elle ne peut pas être utilisée dans un autre appel de la fonction d'exécution.
Doval

2
Le problème avec Dispose(bool disposing)(qui n'est pas défini sur, IDisposablec'est qu'il est utilisé pour traiter le nettoyage des objets gérés et non gérés dont l'objet est un champ (ou dont il est responsable), ce qui permet de résoudre le mauvais problème. les objets non gérés dans un objet géré sans autre objet jetable à Dispose()traiter, alors toutes les méthodes en font partie (faites en sorte que le finaliseur effectue le même nettoyage si nécessaire) ou ayez uniquement des objets gérés à mettre au rebut (ne disposez pas de finaliseur du tout) et le besoin de bool disposingdisparaître.
Jon Hanna

-1 mauvais conseil en raison du fonctionnement de la finalisation. Je suis tout à fait d'accord avec votre idée que l' dispose(disposing)idiome est terribad, mais je le dis parce que les gens utilisent si souvent cette technique et les finaliseurs quand ils ne disposent que de ressources gérées (l' DbConnectionobjet est par exemple géré , il n'est ni invoqué ni commandé, et VOUS DEVEZ SEULEMENT TOUJOURS METTRE EN ŒUVRE UN FINALISATEUR AVEC UN CODE NON GÉRÉ, RAYONNÉ, COM MARSHALLED OU UNSAFE . J'ai expliqué plus haut dans ma réponse à quel point les finaliseurs sont terriblement coûteux. Ne les utilisez pas sauf si vous avez des ressources non gérées dans votre classe.
Jimmy Hoffa

2
Je tiens presque à vous donner +1, simplement parce que vous décryptez quelque chose que tant de gens considèrent comme une chose importante dans l' dispose(dispoing)idiome, mais la vérité est que c'est tellement répandu parce que les gens ont tellement peur des documents GC que quelque chose d'aussi sans rapport que celui cela (il disposedevrait avoir quelque chose à voir avec GC) mérite qu’ils prennent simplement le médicament prescrit sans même l’enquêter. Bon à vous de l’avoir inspecté, mais vous avez raté le plus grand tout (cela encourage les finaliseurs plus souvent qu’ils ne devraient être)
Jimmy Hoffa

1
@ JimmyHoffa merci pour votre contribution. Je conviens qu'un finaliseur ne devrait normalement être utilisé que pour libérer des ressources non gérées, mais ne conviendriez-vous pas que, dans la génération DEBUG, cette règle est inapplicable et que, dans la génération DEBUG, nous devrions être libres d'utiliser les finaliseurs pour détecter les bogues? C’est tout ce que je propose ici, donc je ne vois pas pourquoi vous en contestez la validité. Voir aussi programmers.stackexchange.com/questions/288715/… pour une explication plus longue de cette approche du côté java du monde.
Mike Nakis

0

Pouvez-vous me dire dans quel type de scénario il est en fait une bonne ou raisonnable idée de forcer le ramassage des ordures? Je ne demande pas de cas spécifiques C # mais plutôt tous les langages de programmation qui ont un ramasse-miettes. Je sais que vous ne pouvez pas forcer GC sur toutes les langues, comme Java, mais supposons que vous le puissiez.

Juste pour parler de manière très théorique et en négligeant des problèmes tels que certaines implémentations du GC ralentissant les choses au cours de leurs cycles de collecte, le scénario le plus important auquel je puisse penser pour forcer la récupération de place est un logiciel critique dans lequel les fuites logiques sont préférables aux crashs de pointeur, par exemple, à des moments inattendus pourrait coûter des vies humaines ou quelque chose de ce genre.

Si vous regardez quelques-uns des plus beaux jeux indépendants écrits avec des langages GC comme les jeux Flash, ils coulent comme des fous, mais ils ne plantent pas. Ils pourraient prendre dix fois plus de mémoire 20 minutes de jeu parce qu’une partie de la base de code du jeu avait oublié de définir une référence sur null ou de la supprimer de la liste et les cadences de prise de vue pourraient commencer à souffrir, mais le jeu fonctionne toujours. Un jeu similaire écrit avec un codage C ou C ++ de mauvaise qualité peut planter du fait de l’accès à des pointeurs en suspens du fait 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 dans le cas d'un programme critique, un crash à des moments totalement inattendus pourrait tuer quelqu'un. Je pense donc que les principaux cas de figure seraient des scénarios dans lesquels la sécurité ne serait pas bloquée 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 dans lequel je pense qu'il est mauvais de forcer la GC est pour des choses où la fuite logique est en réalité moins préférable qu'un crash. Avec les jeux, par exemple, le crash ne tuera pas nécessairement quelqu'un et il peut être facilement détecté et réparé lors des tests internes, alors qu'une fuite logique peut ne pas être repérée, 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 crash facilement reproductible 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 serait préférable de forcer GC à une équipe concerne un programme très éphémère, comme tout ce qui est exécuté à partir de la ligne de commande et 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 improbable qu'un logiciel conçu pour être exécuté pendant seulement 3 secondes ne présente jamais de problèmes de fuites logiques, ce qui pourrait rendre les choses plus difficiles. plus simple d’écrire de tels programmes éphémères si l’équipe vient d’utiliser GC.

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.