Quand est-il acceptable d'appeler GC.Collect?

166

Le conseil général est de ne pas appeler à GC.Collectpartir de votre code, mais quelles sont les exceptions à cette règle?

Je ne peux penser qu'à quelques cas très spécifiques où il peut être judicieux de forcer un ramassage des ordures.

Un exemple qui me vient à l'esprit est un service, qui se réveille à intervalles réguliers, effectue une tâche, puis dort pendant une longue période. Dans ce cas, il peut être judicieux de forcer une collecte pour empêcher le processus sur le point d'être inactif de conserver plus de mémoire que nécessaire.

Y a-t-il d'autres cas où il est acceptable d'appeler GC.Collect?

Brian Rasmussen
la source
Possible duplicata de Best Practice for Forcing Garbage Collection in C #
Jimenemex

Réponses:

153

Si vous avez de bonnes raisons de croire qu'un ensemble important d'objets - en particulier ceux que vous pensez appartenir aux générations 1 et 2 - sont désormais éligibles pour le ramasse-miettes, et que ce serait maintenant le moment opportun pour collecter en termes de faible impact sur les performances .

Un bon exemple de ceci est si vous venez de fermer un grand formulaire. Vous savez que tous les contrôles de l'interface utilisateur peuvent désormais être récupérés, et une très courte pause lorsque le formulaire est fermé ne sera probablement pas perceptible pour l'utilisateur.

MISE À JOUR 2.7.2018

À partir de .NET 4.5 - il existe GCLatencyMode.LowLatencyet GCLatencyMode.SustainedLowLatency. Lors de l'entrée et de la sortie de l'un de ces modes, il est recommandé de forcer un GC complet avec GC.Collect(2, GCCollectionMode.Forced).

À partir de .NET 4.6 - il existe la GC.TryStartNoGCRegionméthode (utilisée pour définir la valeur en lecture seule GCLatencyMode.NoGCRegion). Cela peut lui-même, effectuer un garbage collection de blocage complet dans le but de libérer suffisamment de mémoire, mais étant donné que nous interdisons GC pendant un certain temps, je dirais que c'est également une bonne idée d'effectuer un GC complet avant et après.

Source: Ben Watson, ingénieur Microsoft: Writing High-Performance .NET Code , 2nd Ed. 2018.

Voir:

Jon Skeet
la source
8
Selon le code source MS, appeler GC.Collect (2) toutes les 850 ms est très bien. Tu ne crois pas? Ensuite, regardez simplement PresentationCore.dll, MS.Internal.MemoryPressure.ProcessAdd (). J'ai actuellement une application de traitement d'image (petites images, rien avec une pression de mémoire réelle) où l'appel de GC.Collect (2) prend plus de 850 ms et donc l'application entière est gelée par cela (l'application passe 99,7% du temps en GC).
springy76
36
@ springy76: Être fait par Microsoft en un seul endroit ne signifie pas que c'est considéré comme une bonne chose par ceux qui donnent des conseils de Microsoft ...
Jon Skeet
4
Je n'aime pas cet exemple. Quel est le problème à faire après la fermeture du formulaire? Un bon exemple que je vois est celui après le chargement du niveau de jeu sur XBox ou WindowsPhone. Sur ces plates-formes, GC fonctionne après avoir alloué 1 Mo ou qch comme ça. Il est donc bon d'allouer autant que possible lors du chargement du niveau (tout en montrant un écran de démarrage) puis de faire GC.Collect pour essayer d'éviter les collectes pendant le jeu.
Piotr Perak
8
@Peri: Le but de le faire après la fermeture du formulaire est que vous venez de rendre un tas d'objets (contrôles, les données que vous affichaiez) éligibles pour le ramasse-miettes - donc en appelant, GC.Collectvous dites essentiellement au ramasse-miettes que vous savez mieux que ça pour changer. Pourquoi n'aimez-vous pas l'exemple?
Jon Skeet
6
@SHCJ: GC.Collect()demandera que le GC effectue une collecte complète . Si vous savez que vous venez de rendre éligibles à la collecte des ordures de nombreux objets de longue durée de vie et que vous pensez que l'utilisateur est moins susceptible de remarquer une légère pause maintenant que plus tard, il semble tout à fait raisonnable de penser que maintenant c'est mieux il est temps d'inviter une collection plutôt que de la laisser arriver plus tard.
Jon Skeet
50

Je n'utilise GC.Collectque lors de l'écriture de bancs d'essai de performances / profileurs bruts; c'est-à-dire que j'ai deux (ou plus) blocs de code à tester - quelque chose comme:

GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
TestA(); // may allocate lots of transient objects
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
TestB(); // may allocate lots of transient objects
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
...

Donc, TestA()et TestB()courir avec un état aussi similaire que possible - c'est-à-dire TestB()ne pas être martelé simplement parce TestAqu'il l'a laissé très proche du point de basculement.

Un exemple classique serait un simple exe de console (une Mainméthode suffisamment triée pour être publiée ici par exemple), qui montre la différence entre la concaténation de chaînes en boucle et StringBuilder.

Si j'ai besoin de quelque chose de précis, alors ce serait deux tests complètement indépendants - mais souvent cela suffit si nous voulons simplement minimiser (ou normaliser) le GC pendant les tests pour avoir une idée approximative du comportement.

Pendant le code de production? Je ne l'ai pas encore utilisé ;-p

Marc Gravell
la source
1
Et j'ajoute probablement "WaitForPendingFinalizers" (ou quoi que ce soit) aussi dans ce cas ;-p
Marc Gravell
29

La meilleure pratique consiste à ne pas forcer un garbage collection dans la plupart des cas. (Tous les systèmes sur lesquels j'ai travaillé et qui avaient forcé le ramassage des ordures, avaient des problèmes de soulignement qui, s'ils étaient résolus, auraient éliminé le besoin de forcer le ramassage des ordures et accéléré considérablement le système.)

Il existe quelques casvous en savez plus sur l'utilisation de la mémoire que le ramasse-miettes. Il est peu probable que cela soit vrai dans une application multi-utilisateur ou un service qui répond à plusieurs demandes à la fois.

Cependant, dans certains traitements par lots, vous en savez plus que le GC. Par exemple, considérez une application qui.

  • Reçoit une liste de noms de fichiers sur la ligne de commande
  • Traite un seul fichier, puis écrit le résultat dans un fichier de résultats.
  • Lors du traitement du fichier, crée de nombreux objets interconnectés qui ne peuvent pas être collectés tant que le traitement du fichier n'est pas terminé (par exemple, un arbre d'analyse)
  • Ne conserve pas l'état de correspondance entre les fichiers qu'il a traités .

Vous pouvez être faire un cas (après avoir testé minutieusement) que vous devez forcer un garbage collection complet après avoir traité chaque fichier.

Un autre cas est un service qui se réveille toutes les quelques minutes pour traiter certains éléments et ne conserve aucun état pendant son sommeil . Alors forcer une collection complète juste avant d'aller dormir peut être utile.

La seule fois où j'envisagerais de forcer une collection, c'est quand je sais que beaucoup d'objets ont été créés récemment et que très peu d'objets sont actuellement référencés.

Je préférerais avoir une API de ramasse-miettes quand je pourrais lui donner des conseils sur ce type de chose sans avoir à forcer moi-même un GC.

Voir aussi " Les morceaux de performance de Rico Mariani "

Ian Ringrose
la source
19

Un cas est lorsque vous essayez de tester un code qui utilise WeakReference .

Brian
la source
11

Dans les grands systèmes 24/7 ou 24/6 - des systèmes qui réagissent aux messages, aux demandes RPC ou qui interrogent une base de données ou un processus en continu - il est utile de disposer d'un moyen d'identifier les fuites de mémoire. Pour cela, j'ai tendance à ajouter un mécanisme à l'application pour suspendre temporairement tout traitement, puis effectuer un garbage collection complet. Cela met le système dans un état de repos où la mémoire restante est soit une mémoire légitimement longue durée (caches, configuration, etc.), soit une `` fuite '' (objets qui ne sont pas attendus ou dont on ne souhaite pas enraciner mais qui le sont en réalité).

Ce mécanisme facilite grandement le profil de l'utilisation de la mémoire, car les rapports ne seront pas assombri par le bruit du traitement actif.

Pour être sûr d'obtenir toutes les ordures, vous devez effectuer deux collectes:

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

Comme la première collecte entraînera la finalisation de tous les objets avec des finaliseurs (mais pas réellement de ramasser ces objets). Le second GC récupérera ces objets finalisés.

Paul Ruane
la source
J'ai vu la collection en deux passes à quelques endroits maintenant, mais après avoir lu le passage de la documentation MSDN pour GC.WaitForPendingFinalizers qui dit: "Attendez que tous les finaliseurs se terminent avant de continuer. Sans cet appel à GC.WaitForPendingFinalizers, la boucle de travail ci-dessous peut s'exécuter en même temps que les finaliseurs. Avec cet appel, la boucle de travail s'exécute uniquement après que tous les finaliseurs ont été appelés. " Je suis juste un peu paranoïaque. Connaissez-vous une source définitive pour faire deux passes?
jerhewet
1
@jerhewet: La clé pour comprendre pourquoi deux collections sont nécessaires est de comprendre ce qui arrive aux objets avec des finaliseurs. Malheureusement, je n'ai pas exactement ce que vous demandez, mais lisez cet article et cette question sur SO .
Paul Ruane
10

Vous pouvez appeler GC.Collect () lorsque vous savez quelque chose sur la nature de l'application que le garbage collector ne sait pas. Il est tentant de penser que, en tant qu'auteur, c'est très probable. Cependant, la vérité est que le GC est un système expert assez bien écrit et testé, et il est rare que vous sachiez quelque chose sur les chemins de code de bas niveau qu'il ne connaît pas.

Le meilleur exemple auquel je puisse penser où vous pourriez avoir des informations supplémentaires est une application qui alterne entre les périodes d'inactivité et les périodes très occupées. Vous voulez les meilleures performances possibles pour les périodes de pointe et vous voulez donc utiliser le temps d'inactivité pour effectuer un certain nettoyage.

Cependant, la plupart du temps, le GC est assez intelligent pour le faire de toute façon.

Joël Coehoorn
la source
8

En tant que solution de fragmentation de la mémoire. Je sortais des exceptions de mémoire en écrivant beaucoup de données dans un flux mémoire (lecture à partir d'un flux réseau). Les données ont été écrites en blocs de 8 Ko. Après avoir atteint 128 Mo, il y avait une exception même s'il y avait beaucoup de mémoire disponible (mais elle était fragmentée). L'appel de GC.Collect () a résolu le problème. J'ai pu gérer plus de 1G après le correctif.

Ethos
la source
7

Jetez un œil à cet article de Rico Mariani. Il donne deux règles pour appeler GC.Collect (la règle 1 est: "Ne pas"):

Quand appeler GC.Collect ()

M4N
la source
3
J'y suis déjà allé. Je n'essaie pas de trouver des excuses pour faire quelque chose que vous ne devriez pas faire, mais j'aimerais savoir s'il y a des cas spécifiques où cela serait acceptable.
Brian Rasmussen
5

Une instance où il est presque nécessaire d'appeler GC.Collect () est lors de l'automatisation de Microsoft Office via Interop. Les objets COM pour Office n'aiment pas être libérés automatiquement et peuvent entraîner des instances du produit Office occupant de très grandes quantités de mémoire. Je ne sais pas si c'est un problème ou par conception. Il y a beaucoup de messages sur ce sujet sur Internet, donc je n'entrerai pas trop dans les détails.

Lors de la programmation à l'aide d'Interop, chaque objet COM doit être libéré manuellement, généralement via l'utilisation de Marshal.ReleseComObject (). De plus, appeler manuellement Garbage Collection peut aider à "nettoyer" un peu. L'appel du code suivant lorsque vous avez terminé avec les objets Interop semble aider un peu:

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

D'après mon expérience personnelle, l'utilisation d'une combinaison de ReleaseComObject et d'un appel manuel du garbage collection réduit considérablement l'utilisation de la mémoire des produits Office, en particulier Excel.

garthhh
la source
oui, je suis tombé sur cela avec .net accédant à Excel qui fonctionne également via des objets com. Il est important de noter que cela ne fonctionnera pas bien en mode DEBUG car les opérations GC y sont limitées. Il fonctionnera comme prévu uniquement en mode RELEASE. lien pertinent: stackoverflow.com/questions/17130382/…
Blechdose
5

Je faisais des tests de performances sur le tableau et la liste:

private static int count = 100000000;
private static List<int> GetSomeNumbers_List_int()
{
    var lstNumbers = new List<int>();
    for(var i = 1; i <= count; i++)
    {
        lstNumbers.Add(i);
    }
    return lstNumbers;
}
private static int[] GetSomeNumbers_Array()
{
    var lstNumbers = new int[count];
    for (var i = 1; i <= count; i++)
    {
        lstNumbers[i-1] = i + 1;
    }
    return lstNumbers;
}
private static int[] GetSomeNumbers_Enumerable_Range()
{
    return  Enumerable.Range(1, count).ToArray();
}

static void performance_100_Million()
{
    var sw = new Stopwatch();

    sw.Start();
    var numbers1 = GetSomeNumbers_List_int();
    sw.Stop();
    //numbers1 = null;
    //GC.Collect();
    Console.WriteLine(String.Format("\"List<int>\" took {0} milliseconds", sw.ElapsedMilliseconds));

    sw.Reset();
    sw.Start();
    var numbers2 = GetSomeNumbers_Array();
    sw.Stop();
    //numbers2 = null;
    //GC.Collect();
    Console.WriteLine(String.Format("\"int[]\" took {0} milliseconds", sw.ElapsedMilliseconds));

    sw.Reset();
    sw.Start();
//getting System.OutOfMemoryException in GetSomeNumbers_Enumerable_Range method
    var numbers3 = GetSomeNumbers_Enumerable_Range();
    sw.Stop();
    //numbers3 = null;
    //GC.Collect();

    Console.WriteLine(String.Format("\"int[]\" Enumerable.Range took {0} milliseconds", sw.ElapsedMilliseconds));
}

et j'ai obtenu OutOfMemoryExceptiondans la méthode GetSomeNumbers_Enumerable_Range la seule solution de contournement est de désallouer la mémoire en:

numbers = null;
GC.Collect();
Daniel B
la source
Pourquoi voter contre? ma réponse est un exemple qui montre quand appeler GC. Avez-vous une meilleure suggestion? N'hésitez pas à nous présenter.
Daniel B
4

Dans votre exemple, je pense qu'appeler GC.Collect n'est pas le problème, mais plutôt un problème de conception.

Si vous prévoyez de vous réveiller à intervalles (horaires définis), votre programme doit être conçu pour une seule exécution (effectuer la tâche une fois), puis se terminer. Ensuite, vous configurez le programme en tant que tâche planifiée à exécuter aux intervalles planifiés.

De cette façon, vous n'avez pas à vous soucier d'appeler GC.Collect, (ce que vous devriez rarement, voire jamais, faire).

Cela étant dit, Rico Mariani a un excellent article de blog sur ce sujet, que vous pouvez trouver ici:

http://blogs.msdn.com/ricom/archive/2004/11/29/271829.aspx

casperOne
la source
3

Un endroit utile pour appeler GC.Collect () est dans un test unitaire lorsque vous voulez vérifier que vous ne créez pas une fuite de mémoire (par exemple si vous faites quelque chose avec WeakReferences ou ConditionalWeakTable, code généré dynamiquement, etc.).

Par exemple, j'ai quelques tests comme:

WeakReference w = CodeThatShouldNotMemoryLeak();
Assert.IsTrue(w.IsAlive);
GC.Collect();
GC.WaitForPendingFinalizers();
Assert.IsFalse(w.IsAlive);

On pourrait soutenir que l'utilisation de WeakReferences est un problème en soi, mais il semble que si vous créez un système qui repose sur un tel comportement, appeler GC.Collect () est un bon moyen de vérifier ce code.

ChaseMédaillon
la source
2

La réponse courte est: jamais!

BankZ
la source
9
Pas même dans cette situation ?
Roman Starkov
2
using(var stream = new MemoryStream())
{
   bitmap.Save(stream, ImageFormat.Png);
   techObject.Last().Image = Image.FromStream(stream);
   bitmap.Dispose();

   // Without this code, I had an OutOfMemory exception.
   GC.Collect();
   GC.WaitForPendingFinalizers();
   //
}
Denis
la source
2

Il y a des situations où il vaut mieux prévenir que guérir.

Voici une situation.

Il est possible de créer une DLL non gérée en C # à l'aide de réécritures IL (car dans certaines situations, cela est nécessaire).

Supposons maintenant, par exemple, que la DLL crée un tableau d'octets au niveau de la classe - car la plupart des fonctions exportées doivent y accéder. Que se passe-t-il lorsque la DLL est déchargée? Le garbage collector est-il automatiquement appelé à ce stade? Je ne sais pas, mais étant une DLL non gérée, il est tout à fait possible que le GC ne soit pas appelé. Et ce serait un gros problème s'il n'était pas appelé. Lorsque la DLL est déchargée, il en sera de même pour le ramasse-miettes - alors qui sera responsable de la collecte des éventuels déchets et comment le ferait-il? Mieux vaut utiliser le ramasse-miettes de C #. Avoir une fonction de nettoyage (disponible pour le client DLL) où les variables de niveau de classe sont définies sur null et le garbage collector appelé.

Mieux vaut prévenir que guérir.

Carl Looper
la source
2

je suis encore assez incertain à ce sujet. Je travaille depuis 7 ans sur un serveur d'application. Nos plus grandes installations utilisent 24 Go de RAM. Ses appels extrêmement multithread et ALL pour GC.Collect () se sont heurtés à des problèmes de performances vraiment terribles.

De nombreux composants tiers ont utilisé GC.Collect () alors qu'ils pensaient que c'était intelligent de le faire maintenant. Ainsi, un simple tas de rapports Excel bloquait le serveur d'applications pour tous les threads plusieurs fois par minute.

Nous avons dû refactoriser tous les composants tiers afin de supprimer les appels GC.Collect (), et tout a bien fonctionné après cela.

Mais j'utilise également des serveurs sur Win32, et ici j'ai commencé à utiliser beaucoup GC.Collect () après avoir obtenu une OutOfMemoryException.

Mais je suis également assez incertain à ce sujet, car j'ai souvent remarqué que lorsque j'obtiens un MOO sur 32 bits et que je réessaye d'exécuter à nouveau la même opération, sans appeler GC.Collect (), cela fonctionnait très bien.

Une chose que je me demande est l'exception OOM elle-même ... Si j'avais écrit le .Net Framework et que je ne peux pas allouer un bloc de mémoire, j'utiliserais GC.Collect (), défragmenter la mémoire (??), réessayer , et si je ne trouve toujours pas un bloc de mémoire libre, alors je lancerais l'exception OOM.

Ou du moins, faites de ce comportement une option configurable, en raison des inconvénients du problème de performances avec GC.Collect.

Maintenant, j'ai beaucoup de code comme celui-ci dans mon application pour "résoudre" le problème:

public static TResult ExecuteOOMAware<T1, T2, TResult>(Func<T1,T2 ,TResult> func, T1 a1, T2 a2)
{

    int oomCounter = 0;
    int maxOOMRetries = 10;
    do
    {
        try
        {
            return func(a1, a2);
        }
        catch (OutOfMemoryException)
        {
            oomCounter++;
            if (maxOOMRetries > 10)
            {
                throw;
            }
            else
            {
                Log.Info("OutOfMemory-Exception caught, Trying to fix. Counter: " + oomCounter.ToString());
                System.Threading.Thread.Sleep(TimeSpan.FromSeconds(oomCounter * 10));
                GC.Collect();
            }
        }
    } while (oomCounter < maxOOMRetries);

    // never gets hitted.
    return default(TResult);
}

(Notez que le comportement Thread.Sleep () est un comportement vraiment spécifique à l'application, car nous exécutons un service de mise en cache ORM, et le service prend un certain temps pour libérer tous les objets mis en cache, si la RAM dépasse certaines valeurs prédéfinies. quelques secondes la première fois et a augmenté le temps d'attente à chaque occurrence de MOO.)

Thomas Haller
la source
Un composant ne doit pas appeler GC.Collect. Puisqu'il a un effet à l'échelle de l'application, seule l'application devrait le faire (le cas échéant).
CodesInChaos
If i would have written the .Net Framework, and i can't alloc a memory block, i would use GC.Collect(),- Je pense qu'ils le font déjà - j'ai vu des indications selon lesquelles l'un des déclencheurs internes du GC est certains échecs d'allocation de mémoire.
G.Stoynev
2

Vous devriez essayer d'éviter d'utiliser GC.Collect () car c'est très cher. Voici un exemple:

        public void ClearFrame(ulong timeStamp)
    {
        if (RecordSet.Count <= 0) return;
        if (Limit == false)
        {
            var seconds = (timeStamp - RecordSet[0].TimeStamp)/1000;
            if (seconds <= _preFramesTime) return;
            Limit = true;
            do
            {
                RecordSet.Remove(RecordSet[0]);
            } while (((timeStamp - RecordSet[0].TimeStamp) / 1000) > _preFramesTime);
        }
        else
        {
            RecordSet.Remove(RecordSet[0]);

        }
        GC.Collect(); // AVOID
    }

RÉSULTAT DU TEST: UTILISATION DU CPU 12%

Lorsque vous passez à ceci:

        public void ClearFrame(ulong timeStamp)
    {
        if (RecordSet.Count <= 0) return;
        if (Limit == false)
        {
            var seconds = (timeStamp - RecordSet[0].TimeStamp)/1000;
            if (seconds <= _preFramesTime) return;
            Limit = true;
            do
            {
                RecordSet[0].Dispose(); //  Bitmap destroyed!
                RecordSet.Remove(RecordSet[0]);
            } while (((timeStamp - RecordSet[0].TimeStamp) / 1000) > _preFramesTime);
        }
        else
        {
            RecordSet[0].Dispose(); //  Bitmap destroyed!
            RecordSet.Remove(RecordSet[0]);

        }
        //GC.Collect();
    }

RÉSULTAT DU TEST: UTILISATION DU CPU 2-3%

mtekeli
la source
1

Une autre raison est lorsque vous avez un SerialPort ouvert sur un port USB COM, puis que le périphérique USB est débranché. Étant donné que SerialPort a été ouvert, la ressource contient une référence au port précédemment connecté dans le registre du système. Le registre du système sera alors des données périmées , de sorte que la liste des ports disponibles sera erronée. Par conséquent, le port doit être fermé.

L'appel de SerialPort.Close () sur le port appelle Dispose () sur l'objet, mais il reste en mémoire jusqu'à ce que le garbage collection s'exécute réellement, ce qui fait que le registre reste obsolète jusqu'à ce que le garbage collector décide de libérer la ressource.

Depuis https://stackoverflow.com/a/58810699/8685342 :

try
{
    if (port != null)
        port.Close(); //this will throw an exception if the port was unplugged
}
catch (Exception ex) //of type 'System.IO.IOException'
{
    System.GC.Collect();
    System.GC.WaitForPendingFinalizers();
}

port = null;
Jay Tennant
la source
0

Ce n'est pas si pertinent pour la question, mais pour les transformations XSLT dans .NET (XSLCompiledTranform), vous n'aurez peut-être pas le choix. Un autre candidat est le contrôle MSHTML.

Chris S
la source
0

une bonne raison d'appeler GC est sur les petits ordinateurs ARM avec peu de mémoire, comme le Raspberry PI (fonctionnant en mono). Si les fragments de mémoire non alloués utilisent trop de RAM système, alors le système d'exploitation Linux peut devenir instable. J'ai une application où je dois appeler GC toutes les secondes (!) Pour me débarrasser des problèmes de débordement de mémoire.

Une autre bonne solution consiste à jeter les objets lorsqu'ils ne sont plus nécessaires. Malheureusement, ce n'est pas si facile dans de nombreux cas.

harry4516
la source
0

Puisqu'il existe un petit tas d'objets (SOH) et un tas d'objets grands (LOH)

Nous pouvons appeler GC.Collect () pour effacer l'objet de dé-référence dans SOP et déplacer l'objet vécu vers la génération suivante.

Dans .net4.5, nous pouvons également compacter LOH en utilisant largeobjectheapcompactionmode

Max CHien
la source
0

Si vous créez beaucoup de nouveaux System.Drawing.Bitmapobjets, le garbage collector ne les efface pas. Finalement, GDI + pensera que vous manquez de mémoire et lancera une exception «Le paramètre n'est pas valide». Appeler de GC.Collect()temps en temps (pas trop souvent!) Semble résoudre ce problème.

Nacht
la source