Question Comment puis-je récupérer les fichiers .NET de manière agressive?


J'ai une application qui est utilisée dans le traitement de l'image, et je trouve que j'alloue généralement des tableaux de la taille de 4000x4000, ainsi que des flotteurs occasionnels et autres. Actuellement, le framework .NET a tendance à se bloquer dans cette application apparemment aléatoirement, presque toujours avec une erreur de mémoire insuffisante. 32mb n'est pas une déclaration énorme, mais si .NET fragmente la mémoire, il est fort possible que de telles allocations continues ne se comportent pas comme prévu.

Est-il possible de demander au ramasse-miettes d'être plus agressif ou de défragmenter la mémoire (si c'est le problème)? Je me rends compte qu'il y a les appels à GC.Collect et GC.WaitForPendingFinalizers, et je les ai arrosés assez généreusement à travers mon code, mais j'obtiens toujours les erreurs. C'est peut-être parce que j'appelle beaucoup les routines DLL qui utilisent du code natif, mais je ne suis pas sûr. J'ai dépassé ce code C ++, et je m'assure que toute mémoire que je déclare effacée, mais j'obtiens toujours ces crashs C #, donc je suis sûr que ce n'est pas là. Je me demande si les appels C ++ pourraient interférer avec le GC, le faisant quitter la mémoire car il interagissait auparavant avec un appel natif - est-ce possible? Si oui, puis-je désactiver cette fonctionnalité?

MODIFIER: Voici un code très spécifique qui provoquera le crash. Selon cette question SO, Je n'ai pas besoin de disposer des objets BitmapSource ici. Voici la version naïve, pas de GC.Collecte dedans. Il se bloque généralement à l'itération 4 à 10 de la procédure d'annulation. Ce code remplace le constructeur dans un projet WPF vide, car j'utilise WPF. Je fais la folie avec le bitmapsource à cause des limitations que j'ai expliquées dans ma réponse à @dthorpe ci-dessous ainsi que les exigences listées dans cette question SO.

public partial class Window1 : Window {
    public Window1() {
        InitializeComponent();
        //Attempts to create an OOM crash
        //to do so, mimic minute croppings of an 'image' (ushort array), and then undoing the crops
        int theRows = 4000, currRows;
        int theColumns = 4000, currCols;
        int theMaxChange = 30;
        int i;
        List<ushort[]> theList = new List<ushort[]>();//the list of images in the undo/redo stack
        byte[] displayBuffer = null;//the buffer used as a bitmap source
        BitmapSource theSource = null;
        for (i = 0; i < theMaxChange; i++) {
            currRows = theRows - i;
            currCols = theColumns - i;
            theList.Add(new ushort[(theRows - i) * (theColumns - i)]);
            displayBuffer = new byte[theList[i].Length];
            theSource = BitmapSource.Create(currCols, currRows,
                    96, 96, PixelFormats.Gray8, null, displayBuffer,
                    (currCols * PixelFormats.Gray8.BitsPerPixel + 7) / 8);
            System.Console.WriteLine("Got to change " + i.ToString());
            System.Threading.Thread.Sleep(100);
        }
        //should get here.  If not, then theMaxChange is too large.
        //Now, go back up the undo stack.
        for (i = theMaxChange - 1; i >= 0; i--) {
            displayBuffer = new byte[theList[i].Length];
            theSource = BitmapSource.Create((theColumns - i), (theRows - i),
                    96, 96, PixelFormats.Gray8, null, displayBuffer,
                    ((theColumns - i) * PixelFormats.Gray8.BitsPerPixel + 7) / 8);
            System.Console.WriteLine("Got to undo change " + i.ToString());
            System.Threading.Thread.Sleep(100);
        }
    }
}

Maintenant, si j'appelle explicitement le garbage collector, je dois envelopper le code entier dans une boucle externe pour provoquer le crash du MOO. Pour moi, cela a tendance à se produire autour de x = 50 ou plus:

public partial class Window1 : Window {
    public Window1() {
        InitializeComponent();
        //Attempts to create an OOM crash
        //to do so, mimic minute croppings of an 'image' (ushort array), and then undoing the crops
        for (int x = 0; x < 1000; x++){
            int theRows = 4000, currRows;
            int theColumns = 4000, currCols;
            int theMaxChange = 30;
            int i;
            List<ushort[]> theList = new List<ushort[]>();//the list of images in the undo/redo stack
            byte[] displayBuffer = null;//the buffer used as a bitmap source
            BitmapSource theSource = null;
            for (i = 0; i < theMaxChange; i++) {
                currRows = theRows - i;
                currCols = theColumns - i;
                theList.Add(new ushort[(theRows - i) * (theColumns - i)]);
                displayBuffer = new byte[theList[i].Length];
                theSource = BitmapSource.Create(currCols, currRows,
                        96, 96, PixelFormats.Gray8, null, displayBuffer,
                        (currCols * PixelFormats.Gray8.BitsPerPixel + 7) / 8);
            }
            //should get here.  If not, then theMaxChange is too large.
            //Now, go back up the undo stack.
            for (i = theMaxChange - 1; i >= 0; i--) {
                displayBuffer = new byte[theList[i].Length];
                theSource = BitmapSource.Create((theColumns - i), (theRows - i),
                        96, 96, PixelFormats.Gray8, null, displayBuffer,
                        ((theColumns - i) * PixelFormats.Gray8.BitsPerPixel + 7) / 8);
                GC.WaitForPendingFinalizers();//force gc to collect, because we're in scenario 2, lots of large random changes
                GC.Collect();
            }
            System.Console.WriteLine("Got to changelist " + x.ToString());
            System.Threading.Thread.Sleep(100);
        }
    }
}

Si je manipule mal la mémoire dans l'un ou l'autre scénario, s'il y a quelque chose que je devrais repérer avec un profileur, faites-le moi savoir. C'est une routine assez simple ici.

Malheureusement, il semble que la réponse de @ Kevin soit correcte: il s'agit d'un bogue dans .NET et de la manière dont .NET traite les objets de plus de 85 Ko. Cette situation me semble extrêmement étrange; Powerpoint pourrait-il être réécrit en .NET avec ce type de limitation, ou l'une des autres applications de la suite Office? Il ne me semble pas que le 85k soit très volumineux et je pense également que tout programme utilisant des allocations dites «volumineuses» deviendrait instable en l'espace de quelques jours à quelques semaines lors de l'utilisation de .NET.

MODIFIER: Il semble que Kevin a raison, il s’agit d’une limitation du GC de .NET. Pour ceux qui ne veulent pas suivre l'intégralité du thread, .NET dispose de quatre tas de GC: gen0, gen1, gen2 et LOH (Large Object Heap). Tout ce qui est 85k ou moins passe sur l'un des trois premiers tas, en fonction du temps de création (déplacé de gen0 à gen1 à gen2, etc.). Les objets plus grands que 85k sont placés sur le LOH. Le LOH est jamais compacté, donc finalement, les allocations du type que je suis en train de faire finiront par provoquer une erreur de MOO lorsque des objets seront dispersés dans cet espace mémoire. Nous avons constaté que le passage à .NET 4.0 aide quelque peu le problème, en retardant l'exception, mais sans l'empêcher. Pour être honnête, cela ressemble un peu à la barrière 640k - 85k devrait suffire à toute application utilisateur (pour paraphraser cette vidéo d'une discussion du GC en .NET). Pour mémoire, Java ne présente pas ce comportement avec son GC.


40
2018-05-18 20:32


origine


Réponses:


Voici quelques articles détaillant les problèmes avec le tas d'objets volumineux. Cela ressemble à ce que vous pourriez rencontrer.

http://connect.microsoft.com/VisualStudio/feedback/details/521147/large-object-heap-fragmentation-causes-outofmemoryexception

Dangers du tas d'objets volumineux:
http://www.simple-talk.com/dotnet/.net-framework/the-dangers-of-the-large-object-heap/

Voici un lien sur la façon de collecter des données sur le tas d'objets volumineux (LOH):
http://msdn.microsoft.com/en-us/magazine/cc534993.aspx

Selon cela, il semble qu'il n'y ait aucun moyen de compacter le LOH. Je ne trouve rien de plus récent qui dit explicitement comment le faire, et il semble donc qu'il n'a pas changé au cours de l'exécution 2.0:
http://blogs.msdn.com/maoni/archive/2006/04/18/large-object-heap.aspx 

Le moyen le plus simple de résoudre le problème consiste à créer de petits objets, dans la mesure du possible. Votre autre option consiste à ne créer que quelques objets volumineux et à les réutiliser encore et encore. Pas une idée, mais cela pourrait être mieux que de réécrire la structure de l'objet. Comme vous avez dit que les objets créés (tableaux) sont de tailles différentes, cela peut être difficile, mais cela pourrait empêcher l'application de tomber en panne.


22
2018-05-18 20:33



Commencez par restreindre le problème. Si vous avez une fuite de mémoire native, le fait de lancer le GC ne fera rien pour vous.

Exécutez perfmon et examinez la taille du segment de mémoire .NET et les compteurs d'octets privés. Si la taille du tas reste relativement constante mais que les octets privés augmentent, vous avez un problème de code natif et vous devrez séparer les outils C ++ pour le déboguer.

En supposant que le problème est avec le tas .NET, vous devez exécuter un profileur avec le code tel que le profileur Ant de Redgate ou le DotTrace de JetBrain. Cela vous dira quels objets prennent de la place et ne sont pas collectés rapidement. Vous pouvez également utiliser WinDbg avec SOS pour cela, mais c'est une interface délicate (puissante cependant).

Une fois que vous avez trouvé les éléments incriminés, il devrait être plus évident de les traiter. Certaines des choses qui causent des problèmes sont des champs statiques référençant des objets, des gestionnaires d’événements non enregistrés, des objets vivant assez longtemps pour entrer dans Gen2 mais mourant peu après, etc. Sans profil de mémoire, vous ne serez pas capable de repérer la réponse.

Quoi que vous fassiez, les appels GC.Collect "saupoudrent généreusement" sont presque toujours la mauvaise façon de résoudre le problème.

Il y a peu de chances que le passage à la version serveur du GC améliore les choses (juste une propriété dans le fichier de configuration) - la version par défaut de la station de travail est conçue pour garder une interface réactive et renonce efficacement aux grandes colonnes longues.


22
2018-05-18 20:40



Utilisez Process Explorer (à partir de Sysinternals) pour voir ce qu'est le tas d'objets volumineux pour votre application. Votre meilleur pari va rendre vos tableaux plus petits mais en avoir plus. Si vous pouvez éviter d’allouer vos objets sur le LOH, vous n’obtiendrez pas les exceptions OutOfMemoryException et vous n’aurez pas à appeler GC.Collect manuellement.

Le LOH n'est pas compacté et n'alloue que de nouveaux objets à la fin, ce qui signifie que vous pouvez manquer d'espace assez rapidement.


4
2018-05-18 20:44



Si vous allouez une grande quantité de mémoire dans une bibliothèque non gérée (c’est-à-dire la mémoire dont le GC n’est pas au courant), vous pouvez alors faire le GC en a conscience avec le GC.AddMemoryPressure méthode.

Bien sûr, cela dépend quelque peu de ce que fait le code non géré. Vous n'avez pas expressément indiqué qu'il alloue de la mémoire, mais j'ai l'impression que c'est le cas. Si c'est le cas, c'est exactement pour quoi cette méthode a été conçue. Là encore, si la bibliothèque non gérée alloue beaucoup de mémoire, il est également possible qu'elle fragmente la mémoire, ce qui échappe complètement au contrôle du GC, même avec AddMemoryPressure. J'espère que ce n'est pas le cas; si c'est le cas, vous devrez probablement refactoriser la bibliothèque ou changer la façon dont elle est utilisée.

P.S. N'oubliez pas d'appeler GC.RemoveMemoryPression lorsque vous libérez enfin la mémoire non gérée.

(P.P.S. Certaines des autres réponses sont probablement correctes, il y a beaucoup plus de chances que ce soit simplement une fuite de mémoire dans votre code, surtout si c'est le traitement de l'image, je parie que vous ne disposez pas de votre IDIsposable instances. Mais au cas où ces réponses ne vous mèneraient nulle part, c'est une autre voie à suivre.)


3
2018-05-18 21:14



Juste un côté: le ramasse-miettes .NET effectue un GC "rapide" lorsqu'une fonction retourne à son appelant. Cela disposera les vars locaux déclarés dans la fonction.

Si vous structurez votre code de telle sorte que vous ayez une grande fonction qui alloue de gros blocs encore et encore dans une boucle, en attribuant chaque nouveau bloc à la même variable locale, le GC risque de ne pas récupérer les blocs non référencés pendant un certain temps.

Si, par contre, vous structurez votre code de telle sorte que vous ayez une fonction externe avec une boucle qui appelle une fonction interne et que la mémoire est allouée et affectée à une variable locale dans cette fonction interne, le GC doit intervenir immédiatement lorsque le la fonction interne renvoie à l'appelant et récupère le gros bloc de mémoire qui vient d'être alloué, car il s'agit d'une variable locale dans une fonction qui retourne.

Évitez la tentation de jouer avec GC.Collect explicitement.


2
2018-05-18 21:31



Outre la gestion des allocations de manière plus conviviale pour le GC (par exemple, réutilisation des tableaux, etc.), une nouvelle option est désormais disponible: vous pouvez provoquer manuellement le compactage du LOH.

GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;

Cela provoquera un compactage LOH la prochaine fois qu’une collection gen-2 se produira (soit seule, soit par votre appel explicite de GC.Collect).

Notez que ne pas compacter le LOH est Généralement, c'est une bonne idée - votre scénario est tout à fait suffisant pour permettre un compactage manuel. Le LOH est généralement utilisé pour les objets volumineux et de longue durée, tels que les tampons pré-alloués qui sont réutilisés au fil du temps, etc.

Si votre version .NET ne supporte pas encore cela, vous pouvez également essayer d'allouer des tailles de puissances de deux, plutôt que d'affecter précisément la quantité de mémoire dont vous avez besoin. C'est ce que font de nombreux allocateurs natifs pour s'assurer que la fragmentation de la mémoire ne soit pas incroyablement stupide (elle limite fondamentalement la fragmentation maximale du tas). C'est ennuyeux, mais si vous pouvez limiter le code qui gère cela à une petite partie de votre code, c'est une solution de contournement décente.

Notez que vous devez toujours vous assurer qu'il est possible de compacter le tas - toute mémoire épinglée empêchera le compactage dans le tas dans lequel elle se trouve.

Une autre option utile consiste à utiliser la pagination - ne jamais allouer plus de 64 Ko d'espace contigu sur le tas, par exemple; Cela signifie que vous éviterez d'utiliser entièrement le LOH. Ce n'est pas trop difficile de gérer cela dans un simple "tableau-wrapper" dans votre cas. L'essentiel est de maintenir un bon équilibre entre les exigences de performance et l'abstraction raisonnable.

Et bien sûr, en dernier recours, vous pouvez toujours utiliser du code non sécurisé. Cela vous donne beaucoup de souplesse dans la gestion des allocations de mémoire (bien que cela soit un peu plus pénible que, par exemple, en C ++) - notamment en vous permettant d'allouer explicitement de la mémoire non gérée, de travailler avec cela et de libérer la mémoire manuellement. Encore une fois, cela n'a de sens que si vous pouvez isoler ce code sur une petite partie de votre base de code - et assurez-vous d'avoir un wrapper géré sécurisé pour la mémoire, y compris le finaliseur approprié (pour maintenir un niveau de sécurité suffisant) . Ce n'est pas trop difficile en C #, mais si vous vous trouvez trop souvent, il serait peut-être judicieux d'utiliser C ++ / CLI pour ces parties du code et de les appeler depuis votre code C #.


2
2018-06-16 13:19



Avez-vous testé les fuites de mémoire? J'ai utilisé .NET Memory Profiler avec pas mal de succès sur un projet qui présentait un certain nombre de fuites de mémoire très subtiles et persistantes.

Juste comme un test de santé mentale, assurez-vous que vous appelez Dispose sur tous les objets qui implémentent IDisposable.


1
2018-05-18 20:44