100 votes

Fragmentation du tas de gros objets

L'application C#/.NET sur laquelle je travaille souffre d'une fuite de mémoire lente. J'ai utilisé CDB avec SOS pour essayer de déterminer ce qui se passe, mais les données ne semblent pas avoir de sens. J'espérais donc que l'un d'entre vous aurait déjà rencontré ce problème.

L'application est exécutée sur le cadre 64 bits. Elle calcule et sérialise en permanence des données à destination d'un hôte distant et sollicite assez souvent le Large Object Heap (LOH). Cependant, je m'attends à ce que la plupart des objets LOH soient transitoires : une fois le calcul terminé et envoyé à l'hôte distant, la mémoire devrait être libérée. Ce que je vois, cependant, c'est un grand nombre de tableaux d'objets (vivants) intercalés avec des blocs de mémoire libres, par exemple, en prenant un segment aléatoire du LOH :

0:000> !DumpHeap 000000005b5b1000  000000006351da10
         Address               MT     Size
...
000000005d4f92e0 0000064280c7c970 16147872
000000005e45f880 00000000001661d0  1901752 Free
000000005e62fd38 00000642788d8ba8     1056       <--
000000005e630158 00000000001661d0  5988848 Free
000000005ebe6348 00000642788d8ba8     1056
000000005ebe6768 00000000001661d0  6481336 Free
000000005f214d20 00000642788d8ba8     1056
000000005f215140 00000000001661d0  7346016 Free
000000005f9168a0 00000642788d8ba8     1056
000000005f916cc0 00000000001661d0  7611648 Free
00000000600591c0 00000642788d8ba8     1056
00000000600595e0 00000000001661d0   264808 Free
...

Je m'attendrais évidemment à ce que ce soit le cas si mon application créait des objets volumineux à longue durée de vie lors de chaque calcul. (C'est ce qu'elle fait et je reconnais qu'il y aura un certain degré de fragmentation du LOH mais ce n'est pas le problème ici). Le problème est celui des très petits (1056 octets) tableaux d'objets que vous pouvez voir dans le dump ci-dessus, que je ne peux pas voir dans le code être créé et qui restent enracinés d'une manière ou d'une autre.

Notez également que la CDB ne signale pas le type lorsque le segment de tas est vidé : Je ne sais pas si cela est lié ou non. Si je décharge l'objet marqué (<--), CDB/SOS le signale correctement :

0:015> !DumpObj 000000005e62fd38
Name: System.Object[]
MethodTable: 00000642788d8ba8
EEClass: 00000642789d7660
Size: 1056(0x420) bytes
Array: Rank 1, Number of elements 128, Type CLASS
Element Type: System.Object
Fields:
None

Les éléments du tableau d'objets sont tous des chaînes de caractères et ces chaînes sont reconnaissables à partir du code de notre application.

De plus, je suis incapable de trouver leurs racines GC car la commande !GCRoot se bloque et ne revient jamais (j'ai même essayé de la laisser pendant la nuit).

J'apprécierais donc beaucoup que quelqu'un m'explique pourquoi ces petits (<85k) tableaux d'objets se retrouvent dans le LOH : dans quelles situations .NET y placera-t-il un petit tableau d'objets ? Par ailleurs, quelqu'un connaît-il un autre moyen de vérifier l'origine de ces objets ?

Merci d'avance.


Mise à jour 1

Une autre théorie à laquelle j'ai pensé hier, tard dans la journée, est que ces tableaux d'objets étaient initialement volumineux, mais qu'ils ont été réduits, laissant les blocs de mémoire libre que l'on voit dans les vidages de mémoire. Ce qui me rend suspect est que les tableaux d'objets semblent toujours avoir une longueur de 1056 octets (128 éléments), 128 * 8 pour les références et 32 octets d'overhead.

L'idée est qu'un code non sécurisé dans une bibliothèque ou dans le CLR corrompt le champ du nombre d'éléments dans l'en-tête du tableau. C'est un peu tiré par les cheveux, je sais...


Mise à jour 2

Grâce à Brian Rasmussen (voir réponse acceptée), le problème a été identifié comme une fragmentation de la LOH causée par la table interne des chaînes de caractères ! J'ai écrit une application de test rapide pour le confirmer :

static void Main()
{
    const int ITERATIONS = 100000;

    for (int index = 0; index < ITERATIONS; ++index)
    {
        string str = "NonInterned" + index;
        Console.Out.WriteLine(str);
    }

    Console.Out.WriteLine("Continue.");
    Console.In.ReadLine();

    for (int index = 0; index < ITERATIONS; ++index)
    {
        string str = string.Intern("Interned" + index);
        Console.Out.WriteLine(str);
    }

    Console.Out.WriteLine("Continue?");
    Console.In.ReadLine();
}

L'application crée et déréférence d'abord des chaînes uniques dans une boucle. Ceci est juste pour prouver que la mémoire ne fuit pas dans ce scénario. Il est évident qu'elle ne devrait pas fuir et qu'elle ne fuit pas.

Dans la deuxième boucle, les chaînes uniques sont créées et internées. Cette action les enracine dans la table des internes. Ce que je n'ai pas compris, c'est comment la table d'internage est représentée. Il semble qu'elle consiste en un ensemble de pages -- des tableaux d'objets de 128 éléments de chaîne -- qui sont créés dans le LOH. Cela est plus évident dans CDB/SOS :

0:000> .loadby sos mscorwks
0:000> !EEHeap -gc
Number of GC Heaps: 1
generation 0 starts at 0x00f7a9b0
generation 1 starts at 0x00e79c3c
generation 2 starts at 0x00b21000
ephemeral segment allocation context: none
 segment    begin allocated     size
00b20000 00b21000  010029bc 0x004e19bc(5118396)
Large object heap starts at 0x01b21000
 segment    begin allocated     size
01b20000 01b21000  01b8ade0 0x00069de0(433632)
Total Size  0x54b79c(5552028)
------------------------------
GC Heap Size  0x54b79c(5552028)

L'analyse du segment LOH révèle le modèle que j'ai vu dans l'application de fuite :

0:000> !DumpHeap 01b21000 01b8ade0
...
01b8a120 793040bc      528
01b8a330 00175e88       16 Free
01b8a340 793040bc      528
01b8a550 00175e88       16 Free
01b8a560 793040bc      528
01b8a770 00175e88       16 Free
01b8a780 793040bc      528
01b8a990 00175e88       16 Free
01b8a9a0 793040bc      528
01b8abb0 00175e88       16 Free
01b8abc0 793040bc      528
01b8add0 00175e88       16 Free    total 1568 objects
Statistics:
      MT    Count    TotalSize Class Name
00175e88      784        12544      Free
793040bc      784       421088 System.Object[]
Total 1568 objects

Notez que la taille du tableau d'objets est de 528 (au lieu de 1056) parce que ma station de travail est en 32 bits et le serveur d'application en 64 bits. Les tableaux d'objets ont toujours une longueur de 128 éléments.

La morale de cette histoire, c'est qu'il faut faire très attention aux stages. Si la chaîne de caractères que vous intervertissez n'est pas connue pour être membre d'un ensemble fini, votre application présentera des fuites en raison de la fragmentation du LOH, du moins dans la version 2 du CLR.

Dans le cas de notre application, il existe un code général dans le chemin de code de désérialisation qui internalise les identifiants d'entité pendant le désarchivage : Je soupçonne fortement que c'est le coupable. Cependant, les intentions du développeur étaient manifestement bonnes puisqu'il voulait s'assurer que si la même entité est désérialisée plusieurs fois, une seule instance de la chaîne d'identification sera conservée en mémoire.

50voto

Brian Knox Points 1

Pourquoi Microsoft ne peut-il pas nous fournir une simple API dans .NET appelée :

GC.CompactLargeObjectHeap()

même si cette méthode entraîne un blocage de l'application pendant 10 secondes ou plus, c'est mieux que de planter et de devoir redémarrer l'application pour nettoyer la mémoire !

Il existe un grand nombre d'applications qui utilisent des bibliothèques tierces qui effectuent des allocations sur le LOH. Par exemple, même XNA de Microsoft fragmente le LOH à chaque fois que vous faites "new Texture2D()" ou "Texture.FromFile()" sans aucun remède !

Donc, si vous essayez de créer un "moteur de jeu" en .NET, c'est une véritable plaie, car notre LOH perd de la mémoire sans qu'il soit possible de l'arrêter. Tout ce que nous pouvons faire, c'est "ralentir le processus", à moins que nous décidions de "créer notre propre bibliothèque" et d'oublier XNA. Ce n'est certainement pas ce que veut Microsoft !

De nombreux mois de maux de tête seraient résolus pour nous (et pour d'autres !) si MS nous fournissait simplement cette seule méthode ; je le répète :

GC.CompactLargeObjectHeap()

Et pendant que je fais cette suggestion, je suggère que le MS aille encore plus loin et autorise cette commande :

GC.DoLargeObjectHeapCompactionWhenOutOfMemory = true;   // would be false by default

Il suffirait donc de mettre cette valeur à true pour "Visual3D Game Engine" (notre produit), et automatiquement, lorsque la fragmentation provoque la première exception OOM, .NET compacterait automatiquement le LOH, puis réessayerait l'allocation !

Wow, maintenant cela s'alignerait avec tout le battage publicitaire de .NET sur "ça marche tout simplement" et "facile à utiliser"... et ne causerait cette "baisse de performance" que dans des cas extrêmes, où l'alternative est un crash OOM, forçant un redémarrage de l'application !

S'il vous plaît, Microsoft, faites-le pour nous !


S'il vous plaît, n'annoncez pas votre ignorance en donnant le conseil naïf et banal de "utilisez simplement une meilleure gestion de la mémoire" -- aïe ! C'est un conseil très inapproprié ! Vous devriez également dire "écrivez TOUTES vos propres bibliothèques tierces" (par exemple, XNA) ! !!!.

Pourquoi Microsoft n'a-t-il pas déjà fourni ce comportement dans .NET ???? ? Quel mal cela ferait-il ? Cela nous épargnerait ces solutions de fortune très frustrantes et chronophages, qui peuvent au mieux "ralentir les fuites de mémoire", mais jamais les réparer complètement.

La solution que nous demandons serait inoffensive pour ceux qui ne veulent pas l'utiliser, et apporterait un salut rafraîchissant à ceux qui veulent vraiment l'utiliser. S'il vous plaît, s'il vous plaît, s'il vous plaît, fournissez cette solution en .NET 4.0+.

47voto

Brian Rasmussen Points 68853

Le CLR utilise le LOH pour pré-allouer quelques objets (tels que les le tableau utilisé pour les chaînes internées ). Certains d'entre eux sont inférieurs à 85000 octets et ne seraient donc pas normalement alloués sur la LOH.

Il s'agit d'un détail d'implémentation, mais je suppose que la raison en est d'éviter la collecte inutile d'instances qui sont censées survivre aussi longtemps que le processus qu'elles représentent.

En raison également d'une optimisation quelque peu ésotérique, toute double[] de 1000 éléments ou plus est également attribué sur le LOH.

2voto

Daniel Earwicker Points 63298

En lisant les descriptions du fonctionnement de la GC, et la partie sur la façon dont les objets à longue durée de vie finissent dans la génération 2, et la collecte des objets LOH se fait uniquement lors de la collecte complète - tout comme la collecte de la génération 2, l'idée qui vient à l'esprit est... pourquoi ne pas simplement garder la génération 2 et les gros objets dans le même tas, puisqu'ils vont être collectés ensemble ?

Si c'est ce qui se passe réellement, cela expliquerait comment les petits objets se retrouvent au même endroit que le LOH - s'ils ont vécu assez longtemps pour se retrouver dans la génération 2.

Et donc votre problème semble être une assez bonne réfutation de l'idée qui me vient à l'esprit - il en résulterait une fragmentation de la LOH.

Résumé : votre problème pourrait s'expliquer par le fait que la LOH et la génération 2 partagent la même région de tas, bien que cela ne constitue en aucun cas une preuve que c'est l'explication.

Mise à jour : la sortie de !dumpheap -stat fait sortir cette théorie de l'eau ! La génération 2 et le LOH ont leurs propres régions.

1voto

HUAGHAGUAH Points 1369

Si le format est reconnaissable par votre application, pourquoi n'avez-vous pas identifié le code qui génère ce format de chaîne ? S'il y a plusieurs possibilités, essayez d'ajouter des données uniques pour déterminer quel chemin de code est le coupable.

Le fait que les tableaux soient entrelacés de grands éléments libres m'amène à penser qu'ils étaient à l'origine jumelés ou du moins liés. Essayez d'identifier les objets libérés pour comprendre ce qui les a générés et les chaînes associées.

Une fois que vous avez identifié ce qui génère ces chaînes, essayez de comprendre ce qui les empêche d'être GCed. Peut-être sont-elles placées dans une liste oubliée ou inutilisée à des fins de journalisation ou quelque chose de similaire.


EDIT : Ignorez la région de mémoire et la taille spécifique du tableau pour le moment : déterminez simplement ce qui est fait avec ces chaînes de caractères pour causer une fuite. Essayez le !GCRoot quand votre programme a créé ou manipulé ces chaînes une ou deux fois, quand il y a moins d'objets à tracer.

1voto

Ian Ringrose Points 19115

Excellente question, j'ai appris en lisant les questions.

Je pense que d'autres parties du code de désérialisation utilisent également le grand tas d'objets, d'où la fragmentation. Si toutes les chaînes de caractères étaient internées au MÊME moment, je pense que tout irait bien.

Compte tenu de la qualité du garbage collector de .net, le fait de laisser le chemin du code de désérialisation créer un objet chaîne normal devrait suffire. Ne faites rien de plus complexe tant que le besoin n'est pas prouvé.

J'envisagerais tout au plus de conserver une table de hachage des dernières chaînes de caractères que vous avez vues et de les réutiliser. En limitant la taille de la table de hachage et en passant la taille lorsque vous créez la table, vous pouvez arrêter la plupart des fragmentations. Vous devez ensuite trouver un moyen de supprimer de la table de hachage les chaînes que vous n'avez pas vues récemment afin d'en limiter la taille. Mais si les chaînes créées par le chemin du code de désérialisation sont de toute façon de courte durée, vous ne gagnerez pas grand-chose, voire rien.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X