web-dev-qa-db-fra.com

Est correct d'utiliser GC.Collect (); GC.WaitForPendingFinalizers () ;?

J'ai commencé à revoir du code dans un projet et j'ai trouvé quelque chose comme:

GC.Collect();
GC.WaitForPendingFinalizers();

Ces lignes apparaissent généralement sur les méthodes conçues pour détruire l’objet sous prétexte d’augmenter son efficacité. J'ai fait cette remarque:

  1. Appeler explicitement la récupération de place lors de la destruction de chaque objet diminue les performances, car cela n'a pas à être pris en compte si cela est absolument nécessaire pour les performances du CLR.
  2. L'appel de ces instructions dans cet ordre entraîne la destruction de chaque objet uniquement si d'autres objets sont en cours de finalisation. Par conséquent, un objet pouvant être détruit indépendamment doit attendre la destruction d'un autre objet sans nécessité réelle.
  3. Il peut générer une impasse (voir: cette question )

Est-ce que 1, 2 et 3 sont vrais? Pouvez-vous donner une référence à l'appui de vos réponses? 

Bien que je sois presque sûr de mes remarques, je dois être clair dans mes arguments afin d'expliquer à mon équipe pourquoi cela pose problème. C'est la raison pour laquelle je demande confirmation et référence.

28
JPCF

La réponse courte est: sortez-le. Ce code n'améliorera presque jamais les performances, ni l'utilisation de la mémoire à long terme.

Tous vos points sont vrais. (Il peut générer un interblocage; cela ne signifie pas qu'il sera toujours .) L'appel de GC.Collect() recueillera la mémoire de toutes les générations GC. Cela fait deux choses.

  • Il collecte à travers toutes les générations à chaque fois - au lieu de ce que le GC fera par défaut, qui consiste à ne collecter une génération que lorsqu'elle est complète. Une utilisation typique verra Gen0 collecter (environ) dix fois plus souvent que Gen1, qui à son tour collectera (environ) dix fois plus souvent que Gen2. Ce code va collecter toutes les générations à chaque fois. La collection Gen0 est généralement inférieure à 100 ms; Gen2 peut être beaucoup plus long.
  • Il promeut les objets non collectables auprès de la prochaine génération. En d'autres termes, chaque fois que vous forcez une collection et que vous avez toujours une référence à un objet, cet objet sera promu à la génération suivante. En règle générale, cela se produira relativement rarement, mais un code comme celui ci-dessous le forcera beaucoup plus souvent:

    void SomeMethod()
    { 
     object o1 = new Object();
     object o2 = new Object();
    
     o1.ToString();
     GC.Collect(); // this forces o2 into Gen1, because it's still referenced
     o2.ToString();
    }
    

Sans GC.Collect(), ces deux objets seront récupérés à la prochaine occasion. Avec la collection en tant que script, o2 se retrouvera dans Gen1 - ce qui signifie qu'une collection Gen0 automatisée ne sera pas libérer cette mémoire.

Il faut également noter une horreur encore plus grande: en mode DEBUG, le GC fonctionne différemment et ne récupère aucune variable restant dans la portée (même si elle n'est pas utilisée ultérieurement dans la méthode actuelle). Ainsi, en mode DEBUG, le code ci-dessus ne collecterait même pas o1 lors de l'appel de GC.Collect, de sorte que o1 et o2 seront promus. Cela pourrait entraîner une utilisation de la mémoire très imprévisible et inattendue lors du débogage du code. (Des articles tels que this mettent en évidence ce comportement.)

EDIT: Vient juste de tester ce comportement, une certaine ironie: si vous avez une méthode, quelque chose comme ceci:

void CleanUp(Thing someObject)
{
    someObject.TidyUp();
    someObject = null;
    GC.Collect();
    GC.WaitForPendingFinalizers(); 
}

... alors il ne libèrera explicitement PAS la mémoire de someObject, même en mode RELEASE: il en assurera la promotion dans la prochaine génération du GC.

28
Dan Puzey

Il est un point que l’on peut faire et qui est très facile à comprendre: L’exécution de GC nettoie automatiquement de nombreux objets par exécution (par exemple, 10000). L'appeler après chaque destruction nettoie environ un objet par cycle.

Étant donné que le CPG a une surcharge de temps (il est nécessaire d'arrêter et de démarrer les threads, d'analyser tous les objets en vie), le traitement par lots des appels est hautement préférable.

En outre, quoi bien pourrait sortir du nettoyage après chaque objet? Comment cela pourrait-il être plus efficace que le traitement en lots?

8
usr

Votre point 3 est techniquement correct, mais ne peut se produire que si quelqu'un se verrouille pendant un finaliseur.

Même sans ce type d'appel, le verrouillage à l'intérieur d'un finaliseur est encore pire que ce que vous avez ici.

Il arrive très rarement que l'appel de GC.Collect() améliore réellement les performances.

Jusqu'à présent, j'en ai fait 2, peut-être 3 fois dans ma carrière. (Ou peut-être environ 5 ou 6 fois si vous incluez ceux où je l'ai fait, mesuré les résultats, puis repris à nouveau - et c'est quelque chose que vous devriez toujours mesurer après avoir fait).

Dans les cas où vous parcourez des centaines ou des milliers de mégas de mémoire en peu de temps, puis que vous passez à une utilisation beaucoup moins intensive de la mémoire pendant une longue période, cela peut constituer une amélioration importante, voire vitale, pour collecter explicitement. Est-ce ce qui se passe ici?

Partout ailleurs, ils vont au mieux ralentir et utiliser plus de mémoire.

6
Jon Hanna

Voir mon autre réponse ici: 

Pour GC.Collecter ou pas?

lorsque vous appelez vous-même GC.Collect (), deux choses peuvent se produire: vous finissez par passer plus fois à faire des collections (car les collections d'arrière-plan normales auront encore lieu en plus de votre manuel GC.Collect ()) et vous Je vais accrocher à la mémoire plus longtemps (parce que vous avez forcé certaines choses dans une génération d'ordre supérieur qui n'avait pas besoin d'y aller). En d'autres termes, utiliser vous-même GC.Collect () est presque toujours une mauvaise idée.

La seule fois où vous voudrez appeler vous-même GC.Collect (), c’est lorsque vous avez des informations spécifiques sur votre programme qui sont difficiles à connaître pour le récupérateur de place. L'exemple canonique est un programme de longue durée avec des cycles de charge distincts occupés et légers. Vous souhaiterez peut-être forcer une collecte vers la fin d'une période de faible charge, en avance sur un cycle occupé, pour vous assurer que les ressources sont aussi libres que possible pour le cycle occupé. Mais même ici, vous constaterez peut-être que vous faites mieux en repensant la construction de votre application (une tâche planifiée fonctionnerait-elle mieux?).

4
Joel Coehoorn

Je ne l'ai utilisé qu'une seule fois: pour nettoyer le cache côté serveur des documents Crystal Report. Voir ma réponse dans Crystal Reports Exception: la limite maximale de travaux de traitement de rapport configurée par votre administrateur système a été atteinte

WaitForPendingFinalizers m'a été particulièrement utile, car les objets n'étaient parfois pas nettoyés correctement. Compte tenu des performances relativement lentes du rapport sur une page Web, les retards mineurs du GC étaient négligeables et l’amélioration de la gestion de la mémoire a rendu mon serveur plus heureux.

1
gojimmypi

Nous avons rencontré des problèmes similaires à @Grzenio, mais nous travaillons avec des tableaux à deux dimensions beaucoup plus grands, de l’ordre de 1000x1000 à 3000x3000, dans un service Web. 

Ajouter plus de mémoire n'est pas toujours la bonne réponse, vous devez comprendre votre code et le cas d'utilisation. Sans la collecte par GC, nous avons besoin de 16 à 32 Go de mémoire (selon la taille du client). Sans cela, nous aurions besoin de 32 à 64 Go de mémoire et même dans ce cas, rien ne garantit que le système ne souffrira pas. Le ramasse-miettes .NET n'est pas parfait.

Notre service Web dispose d'un cache en mémoire de l'ordre de 5 à 50 millions de chaînes (environ 80 à 140 caractères par paire clé/valeur, en fonction de la configuration). De plus, à chaque demande du client, nous construisions deux matrices, une double, une booléens qui ont ensuite été transférés à un autre service pour effectuer le travail. Pour une "matrice" 1000x1000 (matrice à 2 dimensions), cela correspond à environ 25 Mo, par demande . Le booléen dirait de quels éléments nous avons besoin (en fonction de notre cache). Chaque entrée de cache représente une "cellule" dans la "matrice".

Les performances du cache se dégradent considérablement lorsque le serveur utilise plus de 80% de la mémoire en raison de la pagination. 

Ce que nous avons constaté est que, à moins que nous ne prenions explicitement GC, le récupérateur de mémoire .net ne «nettoyerait» jamais les variables transitoires tant que nous ne serions pas dans la plage de 90 à 95%, point auquel les performances du cache se seraient considérablement dégradées.

Le processus en aval prenant souvent beaucoup de temps (3-900 secondes), l'impact d'une collection sur les performances était négligeable (3-10 secondes par collecte). Nous avons lancé cette collecte après nous avions déjà renvoyé la réponse au client.

En fin de compte, nous avons rendu les paramètres du GC configurables. Avec .net 4.6, il existe d’autres options. Voici le code .net 4.5 que nous avons utilisé.

if (sinceLastGC.Minutes > Service.g_GCMinutes)
{
     Service.g_LastGCTime = DateTime.Now;
     var sw = Stopwatch.StartNew();
     long memBefore = System.GC.GetTotalMemory(false);
     context.Response.Flush();
     context.ApplicationInstance.CompleteRequest();
     System.GC.Collect( Service.g_GCGeneration, Service.g_GCForced ? System.GCCollectionMode.Forced : System.GCCollectionMode.Optimized);
     System.GC.WaitForPendingFinalizers();
     long memAfter = System.GC.GetTotalMemory(true);
     var elapsed = sw.ElapsedMilliseconds;
     Log.Info(string.Format("GC starts with {0} bytes, ends with {1} bytes, GC time {2} (ms)", memBefore, memAfter, elapsed));
}

Après la réécriture pour une utilisation avec .net 4.6, nous séparons la collecte des déchets en 2 étapes: une collecte simple et une collecte de compactage.

    public static RunGC(GCParameters param = null)
    {
        lock (GCLock)
        {
            var theParams = param ?? GCParams;
            var sw = Stopwatch.StartNew();
            var timestamp = DateTime.Now;
            long memBefore = GC.GetTotalMemory(false);
            GC.Collect(theParams.Generation, theParams.Mode, theParams.Blocking, theParams.Compacting);
            GC.WaitForPendingFinalizers();
            //GC.Collect(); // may need to collect dead objects created by the finalizers
            var elapsed = sw.ElapsedMilliseconds;
            long memAfter = GC.GetTotalMemory(true);
            Log.Info($"GC starts with {memBefore} bytes, ends with {memAfter} bytes, GC time {elapsed} (ms)");

        }
    }

    // https://msdn.Microsoft.com/en-us/library/system.runtime.gcsettings.largeobjectheapcompactionmode.aspx
    public static RunCompactingGC()
    {
        lock (CompactingGCLock)
        {
            var sw = Stopwatch.StartNew();
            var timestamp = DateTime.Now;
            long memBefore = GC.GetTotalMemory(false);

            GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
            GC.Collect();
            var elapsed = sw.ElapsedMilliseconds;
            long memAfter = GC.GetTotalMemory(true);
            Log.Info($"Compacting GC starts with {memBefore} bytes, ends with {memAfter} bytes, GC time {elapsed} (ms)");
        }
    }

J'espère que cela aide quelqu'un d'autre, car nous avons passé beaucoup de temps à la recherche.

1
Justin

Dans l’ensemble, vous êtes d’accord avec les réponses: il est très rare que vous deviez faire votre propre collecte des ordures, car l’implémentation python native est relativement efficace et prévisible. Je peux voir deux situations où il est logique de prendre les choses en mains:

  1. Vous disposez d'une application en temps quasi réel et la récupération de place par défaut est déclenchée à un moment inopportun. Vous devriez également vous demander pourquoi vous écrivez du code sensible en temps réel en Python à ce moment-là également; et
  2. Vous déboguez des fuites de mémoire - le nettoyage de la mémoire immédiatement avant de capturer l’utilisation de la mémoire et la recherche d’objets intéressants réduisent l’encombrement; en dehors des fonctionnalités les plus élémentaires, vous feriez mieux d'utiliser des bibliothèques existantes.
0
F1Rumors