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:
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.
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 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.
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?
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.
Voir mon autre réponse ici:
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?).
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.
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.
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: