Il n'y a pas si longtemps, un excellent article est paru sur Habré Optimization of garbage collection dans un service .NET très chargé . Cet article est très intéressant dans la mesure où les auteurs, armés de théorie, ont fait l'impossible plus tôt: ils ont optimisé leur application en utilisant la connaissance du GC. Et si auparavant nous n'avions aucune idée du fonctionnement de ce GC, il nous est maintenant présenté sur un plateau d'argent grâce aux efforts de Konrad Cocos dans son livre Pro .NET Memory Management . Quelles conclusions ai-je tirées pour moi? Faisons une liste des problèmes et réfléchissons à la façon de les résoudre.
Lors du récent atelier CLRium # 5: Garbage Collector, nous avons parlé de GC toute la journée. Cependant, j'ai décidé de publier un rapport avec décodage de texte. Ceci est un discours sur les conclusions concernant l'optimisation des applications.
Réduisez la connectivité entre les générations
Le problème
Pour optimiser la vitesse de collecte des ordures, le GC collecte la plus jeune génération chaque fois que possible. Mais pour ce faire, il a également besoin d'informations sur les liens des générations plus anciennes (elles agissent dans ce cas comme une racine supplémentaire): de la table à cartes.
Dans le même temps, un lien entre la génération plus âgée et la génération plus jeune vous oblige à couvrir la zone avec une table à cartes:
- 4 octets se chevauchent 4 ko ou max. 320 objets - pour l'architecture x86
- 8 octets se chevauchent 8 Ko ou max. 320 objets - pour l'architecture x64
C'est-à -dire GC, vérifiant la table des cartes, rencontrant une valeur non nulle, est obligé de vérifier un maximum de 320 objets pour la présence de liens sortants dans notre génération.
Par conséquent, les liens clairsemés dans la jeune génération rendront le GC plus long
Solution
- Localisez les objets avec des connexions dans la jeune génération - à proximité;
- Si le trafic d'objets de génération zéro est supposé, utilisez la traction. C'est-à -dire faire un pool d'objets (il n'y en aura pas de nouveaux: il n'y aura pas d'objets de génération zéro). Et en outre, en «réchauffant» le pool avec deux GC consécutifs afin que son contenu soit garanti en échec dans la deuxième génération, vous évitez ainsi les liens avec la jeune génération et avez des zéros dans le tableau des cartes;
- Évitez les liens avec la jeune génération;
Évitez une forte connectivité
Le problème
Comme il ressort des algorithmes de la phase de compression des objets en SOH:
- Pour compresser le tas, vous devez faire le tour de l'arborescence et vérifier tous les liens, en les corrigeant pour de nouvelles valeurs
- De plus, les liens de la table de cartes affectent des groupes entiers d'objets
Par conséquent, la forte connectivité générale des objets peut conduire à un affaissement pendant la GC.
Solution
- Avoir des objets fortement connectés à proximité, en une génération
- Évitez les liens inutiles en général (par exemple, au lieu de dupliquer les liens this-> handle, utilisez le déjà existant this-> Service-> handle)
- Évitez le code avec une connectivité cachée. Par exemple, les fermetures
Surveiller l'utilisation des segments
Le problème
Lors de travaux intensifs, une situation peut survenir lorsque l'allocation de nouveaux objets entraîne des retards: l'allocation de nouveaux segments sous le tas et leur dégagement supplémentaire lors du nettoyage des ordures
Solution
- Utilisation de PerfMon / Sysinternal Utilities pour contrôler les points de sélection des nouveaux segments et leur dégagement et libération
- Si nous parlons de LOH, qui est un trafic de tampon dense, utilisez ArrayPool
- Si nous parlons de SOH, assurez-vous que les objets de la même durée de vie sont mis en évidence à proximité, fournissant un balayage au lieu de collecter
- SOH: utiliser des pools d'objets
Ne pas allouer de mémoire dans les sections de code chargées
Le problème
La section de code chargée alloue de la mémoire:
- Par conséquent, GC sélectionne une fenêtre d'allocation non pas de 1 Ko, mais de 8 Ko.
- Si la fenêtre manque d'espace, cela conduit à un GC et à une expansion de la zone fermée
- Un flux dense de nouveaux objets rendra rapidement les objets de courte durée d'autres threads à l'ancienne génération avec des conditions de collecte des ordures moins bonnes
- Ce qui augmentera le temps de collecte des ordures
- Ce qui mènera à arrêter le monde plus longtemps même en mode simultané
Solution
- Interdiction complète de l'utilisation des fermetures dans les sections critiques du code
- Interdiction complète de la boxe sur les sections critiques du code (vous pouvez utiliser l'émulation en tirant si nécessaire)
- Lorsqu'il est nécessaire de créer un objet temporaire pour le stockage de données, utilisez des structures. Mieux vaut ref struct. Lorsque le nombre de champs est supérieur à 2, transmettre par ref
Évitez les allocations de mémoire inutiles dans LOH
Le problème
Placer des tableaux dans LOH conduit à la fragmentation ou à la pondération de la procédure GC
Solution
- Utilisez la division des tableaux en sous-tableaux et une classe qui encapsule la logique de travail avec de tels tableaux (c'est-à -dire, au lieu de List <T>, où le méga-tableau est stocké, sa liste avec le tableau [] [], divisant le tableau un peu plus court)
- Les tableaux iront Ă SOH
- Après quelques collectes d'ordures, ils se coucheront à côté d'objets toujours vivants et cesseront d'influencer la collecte des ordures
- Contrôlez l'utilisation de tableaux doubles avec une longueur de plus de 1000 éléments.
Lorsque cela est justifié et possible, utilisez la pile de threads
Le problème
Il existe un certain nombre d'objets ultra-courts ou d'objets vivant dans un appel de méthode (y compris les appels internes). Ils créent du trafic d'objets
Solution
- Utilisation de l'allocation de mémoire sur la pile dans la mesure du possible:
- Il ne charge pas un tas
- Ne charge pas GC
- Libération de mémoire - Instantanée
- Utilisez
Span T x = stackalloc T[];
au lieu de new T[]
si possible - Utilisez
Span/Memory
si possible - Convertir des algorithmes en types de
ref stack
(StackList: struct, ValueStringBuilder )
Objets gratuits le plus tĂ´t possible
Le problème
Conçus comme éphémères, les objets tombent dans gen1, et parfois dans gen2.
Il en résulte un GC plus lourd qui dure plus longtemps
Solution
- Vous devez libérer la référence d'objet le plus tôt possible
- Si un long algorithme contient du code qui fonctionne avec n'importe quel objet, il est espacé par le code. Mais qui peuvent être regroupés en un seul endroit, il est nécessaire de le regrouper, permettant ainsi de les collecter plus tôt.
- Par exemple, à la ligne 10, la collecte a été supprimée et à la ligne 120, elle a été filtrée.
Pas besoin d'appeler GC.Collect ()
Le problème
Il semble souvent que si vous appelez GC.Collect (), cela corrigera la situation
Solution
- Il est beaucoup plus correct d'apprendre les algorithmes de fonctionnement du GC, de regarder l'application sous ETW et d'autres outils de diagnostic (JetBrains dotMemory, ...)
- Optimiser les zones les plus problématiques
Évitez d'épingler
Le problème
L'épinglage pose un certain nombre de problèmes:
- Complique la collecte des ordures
- Crée des espaces mémoire libres (noeuds éléments de liste libre, table de briques, compartiments)
- Peut laisser certains objets dans la jeune génération, tout en formant des liens à partir de la table à cartes
Solution
S'il n'y a pas d'autre issue, utilisez fixed () {}. Cette méthode de commit ne fait pas de vrai commit: elle ne se produit que lorsque le GC a fonctionné entre accolades.
Évitez la finalisation
Le problème
La finalisation n'est pas appelée de façon déterministe:
- Uninvited Dispose () entraîne la finalisation de tous les liens sortants de l'objet
- Les objets dépendants sont retardés plus longtemps que prévu
- Vieillissement, passage aux générations plus âgées
- Si en même temps ils contiennent des liens vers des plus jeunes, ils génèrent des liens depuis la table des cartes
- Compliquer l'assemblage des générations plus anciennes, les fragmenter et conduire au compactage au lieu du balayage
Solution
Appelez doucement Dispose ()
Évitez trop de threads
Le problème
Avec un grand nombre de threads, le contexte d'allocation se développe, ils sont alloués à chaque thread:
- En conséquence, GC.Collect arrive plus rapidement.
- En raison du manque d'espace dans le segment éphémère, Collect suivra le balayage collectif
Solution
- Contrôlez le nombre de threads par le nombre de cœurs
Évitez le trafic d'objets de différentes tailles
Le problème
Lors du trafic d'objets de tailles et de durées de vie différentes, une fragmentation se produit:
- Augmenter le taux de fragmentation
- Déclenchement de collection avec une phase de changement d'adresse dans tous les objets de référencement
Solution
Si le trafic d'objets est supposé:
- Vérifiez la présence de champs supplémentaires, approximant la taille
- Vérifiez l'absence de manipulation des chaînes: si possible, remplacez par ReadOnlySpan / ReadOnlyMemory
- Relâchez le lien dès que possible
- Profitez de la traction
- Chauffez les caches et les piscines avec un double GC pour compacter les objets. Ainsi, vous évitez les problèmes avec la table à cartes.