Foreplay
Considérez le code suivant:
La signature de la méthode 
Marshal.FinalReleaseComObject est la suivante:
 public static int FinalReleaseComObject(Object o) 
Nous créons un simple objet COM, faisons un peu de travail et le libérons immédiatement. Il semblerait que ce qui pourrait mal tourner? Oui, la création d'un objet à l'intérieur d'une boucle infinie n'est pas une bonne pratique, mais le 
GC prendra tout le sale boulot. La réalité est légèrement différente:

Pour comprendre les fuites de mémoire, vous devez comprendre le fonctionnement de la 
dynamique . Il existe déjà plusieurs articles sur ce sujet sur Habré, par exemple 
celui-ci , mais ils n'entrent pas dans les détails de mise en œuvre, nous allons donc mener nos propres recherches.
Tout d'abord, nous examinerons en détail le mécanisme de travail 
dynamique , puis nous réduirons les connaissances acquises en une seule image et à la fin, nous discuterons des raisons de cette fuite et comment l'éviter. Avant de plonger dans le code, clarifions les données sources: quelle combinaison de facteurs conduit à la fuite?
Les expériences
La création de nombreux objets 
COM natifs est peut-être une mauvaise idée en soi? Vérifions:
 
Tout va bien cette fois:

Revenons à la version originale du code, mais changeons le type d'objet:
 
Et encore une fois, pas de surprise:

Essayons la troisième option:
 
Eh bien maintenant, nous devrions certainement avoir le même comportement! Hein? Non :(

Une image similaire sera si vous déclarez com en tant 
qu'objet ou si vous travaillez avec 
COM géré . Résumez les résultats expérimentaux:
- L'instanciation d'objets COM natifs en soi ne conduit pas à des fuites - le GC réussit à faire face à l'effacement de la mémoire
- Lorsque vous travaillez avec une classe gérée , aucune fuite ne se produit
- Lors de la conversion explicite d'un objet en objet , tout va bien aussi
Pour l'avenir, au premier point, nous pouvons ajouter le fait que travailler avec 
des objets 
dynamiques (appeler des méthodes ou travailler avec des propriétés) ne provoque pas en soi de fuites. La conclusion se suggère: une fuite de mémoire se produit lorsque nous passons un objet 
dynamique (sans conversion de type "manuelle") contenant 
COM natif , comme paramètre de méthode.
Nous devons aller plus loin
Il est temps de se rappeler en 
quoi consiste cette 
dynamique :
Référence rapideC # 4.0 fournit un nouveau type de dynamique . Ce type évite la vérification de type statique par le compilateur. Dans la plupart des cas, il fonctionne comme un type d' objet . Au moment de la compilation, il est supposé qu'un élément déclaré comme dynamique prend en charge toute opération. Cela signifie que vous n'avez pas besoin de penser à la provenance de l'objet - à partir de l'API COM, d'un langage dynamique comme IronPython, à l'aide de la réflexion, ou d'ailleurs. De plus, si le code n'est pas valide, des erreurs seront lancées lors de l'exécution.
Par exemple, si la méthode exampleMethod1 dans le code suivant a exactement un paramètre, le compilateur reconnaît que le premier appel à la méthode ec.exampleMethod1 (10, 4) n'est pas valide car il contient deux paramètres. Cela entraînera une erreur de compilation. Le deuxième appel de méthode, dynamic_ec.exampleMethod1 (10, 4) n'est pas vérifié par le compilateur, car dynamic_ec est donc déclaré comme dynamique . il n'y aura pas d'erreurs de compilation. Néanmoins, l'erreur ne passera pas inaperçue pour toujours - elle sera détectée lors de l'exécution.
 static void Main(string[] args) { ExampleClass ec = new ExampleClass();  
 class ExampleClass { public ExampleClass() { } public ExampleClass(int v) { } public void exampleMethod1(int i) { } public void exampleMethod2(string str) { } } 
 Le code qui utilise 
des variables 
dynamiques subit des changements importants lors de la compilation. Ce code:
 dynamic com = Activator.CreateInstance(comType); Marshal.FinalReleaseComObject(com); 
Se transforme en ce qui suit:
 object instance = Activator.CreateInstance(typeFromClsid);  
Où 
o__0 est la classe statique générée et 
p__0 est le champ statique qu'il 
contient :
 private class o__0 { public static CallSite<Action<CallSite, Type, object>> p__0; } 
Remarque: pour chaque interaction avec dynamique , un champ CallSite est créé. Ceci, comme nous le verrons plus loin, est nécessaire pour optimiser les performances.Notez qu'aucune mention de 
dynamique n'est laissée - notre objet est maintenant stocké dans une variable de type 
objet . Passons en revue le code généré. Tout d'abord, une liaison est créée, qui décrit ce que nous faisons et ce que nous faisons:
 Binder.InvokeMember(CSharpBinderFlags.ResultDiscarded, "FinalReleaseComObject", (IEnumerable<Type>) null, typeof (Foo), (IEnumerable<CSharpArgumentInfo>) new CSharpArgumentInfo[2] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.UseCompileTimeType | CSharpArgumentInfoFlags.IsStaticType, (string) null), CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, (string) null) }) 
Ceci est une description de notre fonctionnement dynamique. Permettez-moi de vous rappeler que nous transmettons une variable 
dynamique à la méthode 
FinalReleaseComObject .
- CSharpBinderFlags.ResultDiscarded - le résultat de l'exécution de la méthode n'est pas utilisé à l'avenir
- "FinalReleaseComObject" - le nom de la méthode appelée
- typeof (Foo) - contexte d'opération; le type d'appel
CSharpArgumentInfo - description des paramètres de liaison. Dans notre cas:
- CSharpArgumentInfo.Create (CSharpArgumentInfoFlags.UseCompileTimeType | CSharpArgumentInfoFlags.IsStaticType, (string) null) - description du premier paramètre - la classe Marshal: elle est statique et son type doit être pris en compte lors de la liaison
- CSharpArgumentInfo.Create (CSharpArgumentInfoFlags.None, (string) null) - description du paramètre de méthode, généralement il n'y a pas d'informations supplémentaires.
S'il ne s'agissait pas d'appeler une méthode, mais d'appeler, par exemple, une propriété à partir d'un objet 
dynamique , il n'y aurait alors qu'un seul 
CSharpArgumentInfo décrivant l'objet 
dynamique lui-même.
CallSite est un wrapper sur une expression dynamique. Il contient deux domaines importants pour nous:
- mise à jour T publique
- public T Target
D'après le code généré, il est clair que lorsqu'une opération est effectuée, 
Target est appelé avec des paramètres le décrivant:
 Foo.o__0.p__0.Target((CallSite) Foo.o__0.p__0, typeof (Marshal), instance); 
En combinaison avec 
CSharpArgumentInfo décrit ci-dessus 
, ce code signifie ce qui suit: vous devez appeler la méthode FinalReleaseComObject sur la classe Marshal statique avec le paramètre d'instance. Au moment du premier appel, le même délégué est stocké dans 
Target comme dans 
Update . Le délégué de 
mise à jour est responsable de deux tâches importantes:
- Lier une opération dynamique à une opération statique (le mécanisme d'enchère lui-même dépasse le cadre de cet article)
- Formation de cache
Nous nous intéressons au deuxième point. Il convient de noter ici que lorsque vous travaillez avec un objet dynamique, nous devons vérifier la validité de l'opération à chaque fois. Il s'agit d'une tâche plutôt gourmande en ressources, je souhaite donc mettre en cache les résultats de ces vérifications. En ce qui concerne l'appel d'une méthode avec un paramètre, nous devons nous rappeler ce qui suit:
- Le type sur lequel la méthode est appelée
- Le type d'objet transmis par le paramètre (pour être sûr qu'il peut être converti en type de paramètre)
- L'opération est-elle valide
Ensuite, lorsque vous appelez à nouveau 
Target , nous n'avons pas besoin d'effectuer des liaisons relativement coûteuses: il suffit de comparer les types et, s'ils correspondent, d'appeler la fonction objectif. Pour résoudre ce problème, un 
ExpressionTree est créé pour chaque opération dynamique, qui stocke les 
contraintes et la 
fonction objectif à laquelle l'expression dynamique était liée.
Cette fonction peut être de deux types:
- Erreur de liaison : par exemple, une méthode est appelée sur un objet dynamique qui n'existe pas ou un objet dynamique ne peut pas être converti dans le type du paramètre auquel il est transmis: vous devez alors lever une exception comme Microsoft.CSharp.RuntimeBinderException: 'NoSuchMember'
- Le défi est légal: il suffit alors d'effectuer l'action requise
Cet 
ExpressionTree est formé lors de l'exécution du délégué de 
mise à jour et stocké dans 
Target . 
Cible - Cache 
L0 , nous parlerons plus en détail du cache plus tard.
Ainsi, 
Target stocke le dernier 
ExpressionTree généré via le délégué de 
mise à jour . Voyons à quoi ressemble cette 
règle comme un exemple de type 
Managed passé à la méthode 
Boo :
 public class Foo { public void Test() { var type = typeof(int); dynamic instance = Activator.CreateInstance(type); Boo(instance); } public void Boo(object o) { } } 
 .Lambda CallSite.Target<System.Action`3[Actionsss.CallSite,ConsoleApp12.Foo,System.Object]>( Actionsss.CallSite $$site, ConsoleApp12.Foo $$arg0, System.Object $$arg1) { .Block() { .If ($$arg0 .TypeEqual ConsoleApp12.Foo && $$arg1 .TypeEqual System.Int32) { .Return #Label1 { .Block() { .Call $$arg0.Boo((System.Object)((System.Int32)$$arg1)); .Default(System.Object) } } } .Else { .Default(System.Void) }; .Block() { .Constant<Actionsss.Ast.Expression>(IIF((($arg0 TypeEqual Foo) AndAlso ($arg1 TypeEqual Int32)), returnUnamedLabel_0 ({ ... }) , default(Void))); .Label .LabelTarget CallSiteBinder.UpdateLabel: }; .Label .If ( .Call Actionsss.CallSiteOps.SetNotMatched($$site) ) { .Default(System.Void) } .Else { .Invoke (((Actionsss.CallSite`1[System.Action`3[Actionsss.CallSite,ConsoleApp12.Foo,System.Object]])$$site).Update)( $$site, $$arg0, $$arg1) } .LabelTarget #Label1: } } 
Le bloc le plus important pour nous:
 .If ($$arg0 .TypeEqual ConsoleApp12.Foo && $$arg1 .TypeEqual System.Int32) 
$$ arg0 et 
$$ arg1 sont les paramètres avec lesquels 
Target est appelé:
 Foo.o__0.p__0.Target((CallSite) Foo.o__0.p__0, <b>this</b>, <b>instance</b>); 
Traduit en humain, cela signifie ce qui suit:
Nous avons déjà vérifié que si le premier paramètre est de type 
Foo et le second est 
Int32 , alors vous pouvez appeler 
Boo en toute sécurité 
((object) $$ arg1) .
 .Return #Label1 { .Block() { .Call $$arg0.Boo((System.Object)((System.Int32)$$arg1)); .Default(System.Object) } 
Remarque: en cas d'erreur de liaison, le bloc Label1 ressemble à ceci: .Return #Label1 { .Throw .New Microsoft.CSharp.RuntimeBinderException("NoSuchMember") 
Ces vérifications sont appelées 
contraintes . 
Il existe deux types de 
restrictions : par type d'objet et par instance spécifique de l'objet (l'objet doit être exactement le même). Si au moins une des restrictions échoue, nous devrons revérifier la validité de l'expression dynamique, pour cela nous appellerons le délégué de 
mise à jour . Selon le schéma que nous connaissons déjà, il effectuera la liaison avec de nouveaux types et enregistrera le nouvel 
ExpressionTree dans 
Target .
Cache
Nous avons déjà découvert que 
Target est un 
cache L0 . Chaque fois que 
Target est appelé, la première chose que nous ferons est de passer par les restrictions qui y sont déjà stockées. Si les restrictions échouent et qu'une nouvelle liaison est générée, l'ancienne règle passe simultanément à 
L1 et 
L2 . À l'avenir, lorsque vous 
manquerez le cache 
L0 , les règles de 
L1 et 
L2 seront recherchées jusqu'à ce que celle qui convient soit trouvée.
- L1 : Les dix dernières règles qui ont quitté L0 (stockées directement dans CallSite )
- L2 : Les 128 dernières règles créées à l'aide d'une instance de classeur spécifique (qui est CallSiteBinder , unique à chaque CallSite )
Maintenant, nous pouvons enfin ajouter ces détails dans un seul ensemble et décrire sous la forme d'un algorithme ce qui se passe lorsque 
Foo.Bar (someDynamicObject) est appelé :
1. Un classeur est créé qui se souvient du contexte et de la méthode appelée au niveau de leurs signatures
2. La première fois que l'opération est appelée, 
ExpressionTree est créé, qui stocke:
2.1 
Limitations . Dans ce cas, il s'agira de deux restrictions sur le type de paramètres de liaison actuels
2.2 
Fonction objective : soit 
lever une exception (dans ce cas c'est impossible, car toute 
dynamique aboutira avec succès à l'objet) soit un appel à la méthode 
Bar3. Compilez et exécutez le ExpressionTree résultant
4. Lorsque vous rappelez l'opération, deux options sont possibles:
4.1 
Limitations fonctionnelles : appelez simplement 
Bar4.2 Les 
limitations n'ont pas fonctionné : répétez l'étape 2 pour les nouveaux paramètres de liaison
Ainsi, avec l'exemple du type 
Géré , il est devenu à peu près clair comment la 
dynamique fonctionne de l'intérieur. Dans le cas décrit, nous ne manquerons jamais le cache, car les types sont toujours les mêmes *, donc 
Update sera appelé exactement une fois lorsque 
CallSite est initialisé. Ensuite, pour chaque appel, seules les restrictions seront vérifiées et la fonction objectif sera appelée immédiatement. Ceci est en excellent accord avec nos observations de mémoire: pas de calcul - pas de fuites.
* Pour cette raison, le compilateur génère ses CallSites pour chacun: la probabilité de manquer le cache L0 est extrêmement réduiteIl est temps de découvrir en quoi ce schéma diffère dans le cas des objets 
COM natifs . Jetons un coup d'œil à 
ExpressionTree :
 .Lambda CallSite.Target<System.Action`3[Actionsss.CallSite,ConsoleApp12.Foo,System.Object]>( Actionsss.CallSite $$site, ConsoleApp12.Foo $$arg0, System.Object $$arg1) { .Block() { .If ($$arg0 .TypeEqual ConsoleApp12.Foo && .Block(System.Object $var1) { $var1 = .Constant<System.WeakReference>(System.WeakReference).Target; $var1 != null && (System.Object)$$arg1 == $var1 }) { .Return #Label1 { .Block() { .Call $$arg0.Boo((System.__ComObject)$$arg1); .Default(System.Object) } } } .Else { .Default(System.Void) }; .Block() { .Constant<Actionsss.Ast.Expression>(IIF((($arg0 TypeEqual Foo) AndAlso {var Param_0; ... }), returnUnamedLabel_1 ({ ... }) , default(Void))); .Label .LabelTarget CallSiteBinder.UpdateLabel: }; .Label .If ( .Call Actionsss.CallSiteOps.SetNotMatched($$site) ) { .Default(System.Void) } .Else { .Invoke (((Actionsss.CallSite`1[System.Action`3[Actionsss.CallSite,ConsoleApp12.Foo,System.Object]])$$site).Update)( $$site, $$arg0, $$arg1) } .LabelTarget #Label1: } } 
On peut voir que la différence ne concerne que la deuxième restriction:
 .If ($$arg0 .TypeEqual ConsoleApp12.Foo && .Block(System.Object $var1) { $var1 = .Constant<System.WeakReference>(System.WeakReference).Target; $var1 != null && (System.Object)$$arg1 == $var1 }) 
Si dans le cas du code 
managé , nous avions deux restrictions sur le type d'objets, alors nous voyons ici que la deuxième restriction vérifie l'équivalence des instances via 
WeakReference .
Remarque: La restriction d'instance en plus des objets COM est également utilisée pour TransparentProxyEn pratique, sur la base de notre connaissance du fonctionnement du cache, cela signifie que chaque fois que nous recréons un objet 
COM dans une boucle, nous manquerons le cache 
L0 (et 
L1 / L2 aussi, car les anciennes règles avec liens y seront stockées aux anciennes instances). La première hypothèse qui vous demande dans la tête est que le cache de règles coule. Mais le code y est assez simple et tout va bien: les anciennes règles sont supprimées correctement. Dans le même temps, l'utilisation de 
WeakReference dans 
ExpressionTree n'empêche pas le 
GC de collecter des objets inutiles.
Le mécanisme d'enregistrement des règles dans le cache L1: const int MaxRules = 10; internal void AddRule(T newRule) { T[] rules = Rules; if (rules == null) { Rules = new[] { newRule }; return; } T[] temp; if (rules.Length < (MaxRules - 1)) { temp = new T[rules.Length + 1]; Array.Copy(rules, 0, temp, 1, rules.Length); } else { temp = new T[MaxRules]; Array.Copy(rules, 0, temp, 1, MaxRules - 1); } temp[0] = newRule; Rules = temp; } 
Alors, quel est le problème? Essayons de clarifier l'hypothèse: une fuite de mémoire se produit quelque part lors de la liaison d'un objet 
COM .
Expériences, partie 2
Encore une fois, passons des conclusions spéculatives aux expériences. Tout d'abord, répétons ce que le compilateur fait pour nous:
 
Nous vérifions:

La fuite a été conservée. Juste. Mais quelle en est la raison? Après avoir étudié le code des classeurs (que nous laissons derrière les crochets), il est clair que la seule chose qui affecte le type de notre objet est l'option de restriction. Peut-être que ce n'est pas une question d'objets 
COM , mais un liant? Il n'y a pas beaucoup de choix, provoquons plusieurs liaisons pour le type 
Géré :
 while (true) { object instance = Activator.CreateInstance(typeof(int)); var autogeneratedBinder = Binder.InvokeMember(CSharpBinderFlags.ResultDiscarded, "Boo", null, typeof(Foo), new CSharpArgumentInfo[2] { CSharpArgumentInfo.Create( CSharpArgumentInfoFlags.UseCompileTimeType, null), CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) }); var callSite = CallSite<Action<CallSite, Foo, object>>.Create(autogeneratedBinder); callSite.Target(callSite, this, instance); } 

Ouah! Il semble que nous l'avons attrapé. Le problème n'est pas du tout avec l' 
objet COM , comme il nous a semblé au départ, juste en raison des limitations de l'instance, c'est le seul cas où la liaison se produit plusieurs fois à l'intérieur de notre boucle. Dans tous les autres cas, j'ai récupéré le 
cache L0 et effectué une liaison.
Conclusions
Fuite de mémoire
Si vous travaillez avec 
des variables 
dynamiques qui contiennent 
COM natif ou 
TransparentProxy , ne les passez jamais en tant que paramètres de méthode. Si vous devez toujours le faire, utilisez la conversion explicite en 
objet , puis le compilateur sera en retard sur vous.
Mauvais :
 dynamic com = Activator.CreateInstance(comType);  
Correctement :
 dynamic com = Activator.CreateInstance(comType);  
Par mesure de précaution supplémentaire, essayez d'instancier ces objets aussi rarement que possible. Réel pour toutes les versions du 
.NET Framework . (Pour l'instant) n'est pas très pertinent pour. 
NET Core , car il n'y a 
pas de prise en charge pour les objets 
COM dynamiques .
Performances
Il est dans votre intérêt que les échecs de cache se produisent aussi rarement que possible, car dans ce cas, il n'est pas nécessaire de trouver une règle appropriée dans les caches de haut niveau. Des échecs dans le cache 
L0 se produiront principalement en cas de non-concordance du type de l'objet 
dynamique avec les restrictions préservées.
 dynamic com = GetSomeObject(); public object GetSomeObject() {  
Cependant, dans la pratique, vous ne remarquerez probablement pas la différence de performances sauf si le nombre d'appels à cette fonction est mesuré en millions ou si la variabilité des types n'est pas inhabituellement grande. Les coûts en cas de manque sur le cache 
L0 sont tels, 
N est le nombre de types:
- N <10. Si vous manquez, parcourez uniquement les règles de cache L1 existantes
- 10 < N <128 . Énumération du cache L1 et L2 (maximum 10 et N itérations). Création et remplissage d'un tableau de 10 éléments
- N > 128. Itérer sur le cache L1 et L2 . Créez et remplissez des tableaux de 10 et 128 éléments. Si vous manquez le cache L2, reliez à nouveau
Dans les deuxième et troisième cas, la charge sur le CPG augmentera.
Conclusion
Malheureusement, nous n'avons pas trouvé de véritable raison de la fuite de mémoire, cela nécessitera une étude séparée du liant. Heureusement, 
WinDbg fournit un indice pour une enquête plus approfondie: quelque chose de mauvais se produit dans le 
DLR . La première colonne est le nombre d'objets

Bonus
Pourquoi la conversion en objet empêche-t-elle explicitement une fuite?N'importe quel type peut être converti en 
objet , de sorte que l'opération cesse d'être dynamique.
Pourquoi n'y a-t-il pas de fuites lors de l'utilisation des champs et des méthodes d'un objet COM?Voici à quoi ressemble 
ExpressionTree pour l'accès aux champs:
 .If ( .Call System.Dynamic.ComObject.IsComObject($$arg0) ) { .Return #Label1 { .Dynamic GetMember ComMarks(.Call System.Dynamic2.ComObject.ObjectToComObject($$arg0)) } }