Detail dinamis: game kompiler rahasia, kebocoran memori, nuansa kinerja

Foreplay



Pertimbangkan kode berikut:

//Any native COM object var comType = Type.GetTypeFromCLSID(new Guid("E13B6688-3F39-11D0-96F6-00A0C9191601")); while (true) { dynamic com = Activator.CreateInstance(comType); //do some work Marshal.FinalReleaseComObject(com); } 


Tanda tangan metode Marshal.FinalReleaseComObject adalah sebagai berikut:

 public static int FinalReleaseComObject(Object o) 


Kami membuat objek COM sederhana, melakukan beberapa pekerjaan, dan segera melepaskannya. Tampaknya apa yang salah? Ya, membuat objek di dalam infinite loop bukanlah praktik yang baik, tetapi GC akan mengambil semua pekerjaan kotor. Kenyataannya sedikit berbeda:



Untuk memahami mengapa memori bocor, Anda perlu memahami cara kerja dinamis . Sudah ada beberapa artikel tentang hal ini di Habré, misalnya yang ini , tetapi mereka tidak membahas detail implementasi, jadi kami akan melakukan penelitian kami sendiri.



Pertama, kita akan memeriksa secara rinci mekanisme kerja yang dinamis , kemudian kita akan mengurangi pengetahuan yang diperoleh menjadi satu gambar dan pada akhirnya kita akan membahas alasan kebocoran ini dan bagaimana cara menghindarinya. Sebelum masuk ke dalam kode, mari kita perjelas data sumber: kombinasi faktor apa yang menyebabkan kebocoran?

Eksperimennya



Mungkin membuat banyak objek COM asli adalah ide yang buruk? Mari kita periksa:

 //Any native COM object var comType = Type.GetTypeFromCLSID(new Guid("E13B6688-3F39-11D0-96F6-00A0C9191601")); while (true) { dynamic com = Activator.CreateInstance(comType); } 


Semuanya baik saat ini:



Mari kembali ke versi asli kode, tetapi ubah jenis objek:

 //Any managed type include managed COM var type = typeof(int); while (true) { dynamic com = Activator.CreateInstance(type); //do some work Marshal.FinalReleaseComObject(com); } 


Dan lagi, tidak ada kejutan:



Mari kita coba opsi ketiga:

 //Simple COM object var comType = Type.GetTypeFromCLSID(new Guid("435356F9-F33F-403D-B475-1E4AB512FF95")); while (true) { dynamic com = Activator.CreateInstance(comType); //do some work Marshal.FinalReleaseComObject((object) com); } 


Nah sekarang, kita pasti harus mendapatkan perilaku yang sama! Hah? Tidak :(



Gambaran serupa akan terjadi jika Anda mendeklarasikan com sebagai objek atau jika Anda bekerja dengan COM Managed . Ringkas hasil eksperimen:

  1. Membuat instance objek COM asli dengan sendirinya tidak menyebabkan kebocoran - GC berhasil mengatasi dengan membersihkan memori
  2. Saat bekerja dengan kelas Managed apa pun, kebocoran tidak terjadi
  3. Ketika secara eksplisit melemparkan objek ke objek , semuanya juga baik-baik saja


Ke depan, ke poin pertama kita dapat menambahkan fakta bahwa bekerja dengan objek dinamis (memanggil metode atau bekerja dengan properti) dengan sendirinya juga tidak menyebabkan kebocoran. Kesimpulannya menunjukkan sendiri: kebocoran memori terjadi ketika kita melewati objek dinamis (tanpa konversi tipe "manual") yang mengandung COM asli , sebagai parameter metode.

Kita harus masuk lebih dalam



Saatnya untuk mengingat apa arti dinamika ini :

Referensi cepat
C # 4.0 menyediakan tipe dinamis baru . Tipe ini menghindari pengecekan tipe statis oleh kompiler. Dalam kebanyakan kasus, ini berfungsi sebagai tipe objek . Pada waktu kompilasi, diasumsikan bahwa elemen yang dinyatakan dinamis mendukung operasi apa pun. Ini berarti Anda tidak perlu memikirkan dari mana objek itu berasal - dari COM API, bahasa dinamis seperti IronPython, menggunakan refleksi, atau dari tempat lain. Selain itu, jika kode tidak valid, kesalahan akan dilempar ke dalam runtime.

Sebagai contoh, jika metode exampleMethod1 dalam kode berikut memiliki tepat satu parameter, kompiler mengakui bahwa panggilan pertama ke metode ec.exampleMethod1 (10, 4) tidak valid karena mengandung dua parameter. Ini akan menghasilkan kesalahan kompilasi. Pemanggilan metode kedua, dynamic_ec.exampleMethod1 (10, 4) tidak diperiksa oleh kompiler, karena dynamic_ec dinyatakan dinamis , oleh karena itu. tidak akan ada kesalahan kompilasi. Namun demikian, kesalahan tidak akan luput dari perhatian selamanya - itu akan terdeteksi di runtime.

 static void Main(string[] args) { ExampleClass ec = new ExampleClass(); //      ,  exampleMethod1    . //ec.exampleMethod1(10, 4); dynamic dynamic_ec = new ExampleClass(); //      ,  //      dynamic_ec.exampleMethod1(10, 4); //        ,  //  ,      dynamic_ec.someMethod("some argument", 7, null); dynamic_ec.nonexistentMethod(); } 


 class ExampleClass { public ExampleClass() { } public ExampleClass(int v) { } public void exampleMethod1(int i) { } public void exampleMethod2(string str) { } } 




Kode yang menggunakan variabel dinamis mengalami perubahan signifikan selama kompilasi. Kode ini:

 dynamic com = Activator.CreateInstance(comType); Marshal.FinalReleaseComObject(com); 


Berubah menjadi sebagai berikut:

 object instance = Activator.CreateInstance(typeFromClsid); // ISSUE: reference to a compiler-generated field if (Foo.o__0.p__0 == null) { // ISSUE: reference to a compiler-generated field Foo.o__0.p__0 = CallSite<Action<CallSite, Type, object>>.Create(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) })); } // ISSUE: reference to a compiler-generated field // ISSUE: reference to a compiler-generated field Foo.o__0.p__0.Target((CallSite) Foo.o__0.p__0, typeof (Marshal), instance); 


Di mana o__0 adalah kelas statis yang dihasilkan, dan p__0 adalah bidang statis di dalamnya:

 private class o__0 { public static CallSite<Action<CallSite, Type, object>> p__0; } 


Catatan: untuk setiap interaksi dengan dinamis , bidang CallSite dibuat. Ini, seperti yang akan dilihat nanti, diperlukan untuk mengoptimalkan kinerja.

Perhatikan bahwa tidak ada lagi dinamika yang tersisa - objek kita sekarang disimpan dalam variabel tipe objek . Mari kita telusuri kode yang dihasilkan. Pertama, sebuah ikatan dibuat, yang menggambarkan apa dan apa yang kita lakukan:

 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) }) 


Ini adalah deskripsi operasi dinamis kami. Biarkan saya mengingatkan Anda bahwa kami melewati variabel dinamis ke metode FinalReleaseComObject .

  • CSharpBinderFlags.ResultDiscarded - hasil dari eksekusi metode tidak digunakan di masa depan
  • "FinalReleaseComObject" - nama metode yang disebut
  • typeof (Foo) - konteks operasi; jenis panggilan


CSharpArgumentInfo - deskripsi parameter yang mengikat. Dalam kasus kami:

  • CSharpArgumentInfo.Create (CSharpArgumentInfoFlags.GunakanCompileTimeType | CSharpArgumentInfoFlags.IsStaticType, (string) null) - deskripsi parameter pertama - kelas Marshal: statis dan jenisnya harus dipertimbangkan saat mengikat
  • CSharpArgumentInfo.Create (CSharpArgumentInfoFlags.None, (string) null) - deskripsi parameter metode, biasanya tidak ada informasi tambahan.


Jika itu bukan masalah memanggil metode, tetapi, misalnya, memanggil properti dari objek dinamis , maka hanya akan ada satu CSharpArgumentInfo yang menggambarkan objek dinamis itu sendiri.

CallSite adalah pembungkus ekspresi dinamis. Ini berisi dua bidang penting bagi kami:

  • T publik
  • Target T publik


Dari kode yang dihasilkan, jelas bahwa ketika operasi apa pun dilakukan, Target dipanggil dengan parameter yang menggambarkannya:

 Foo.o__0.p__0.Target((CallSite) Foo.o__0.p__0, typeof (Marshal), instance); 


Sehubungan dengan CSharpArgumentInfo yang dijelaskan di atas , kode ini berarti yang berikut: Anda perlu memanggil metode FinalReleaseComObject pada kelas Marshal statis dengan parameter instance. Pada saat panggilan pertama, delegasi yang sama disimpan di Target seperti dalam Pembaruan . Delegasi Pembaruan bertanggung jawab atas dua tugas penting:

  1. Mengikat operasi dinamis ke operasi statis (mekanisme pengikatan berada di luar cakupan artikel ini)
  2. Formasi Cache


Kami tertarik pada poin kedua. Perlu dicatat di sini bahwa ketika bekerja dengan objek dinamis, kita perlu memeriksa validitas operasi setiap kali. Ini adalah tugas yang agak intensif sumber daya, jadi saya ingin men-cache hasil pemeriksaan tersebut. Berkenaan dengan memanggil metode dengan parameter, kita perlu mengingat yang berikut:

  1. Jenis yang disebut metode ini
  2. Jenis objek yang dilewatkan oleh parameter (untuk memastikan bahwa itu dapat dilemparkan ke jenis parameter)
  3. Apakah operasinya valid


Kemudian, ketika memanggil Target lagi, kita tidak perlu melakukan binding yang relatif mahal: cukup bandingkan tipenya dan, jika cocok, panggil fungsi objektif. Untuk mengatasi masalah ini, ExpressionTree dibuat untuk setiap operasi dinamis, yang menyimpan kendala dan fungsi tujuan yang terikat ekspresi dinamis.

Fungsi ini dapat terdiri dari dua jenis:

  • Binding error : misalnya, suatu metode dipanggil pada objek dinamis yang tidak ada atau objek dinamis tidak dapat dikonversi ke jenis parameter yang dilewati: maka Anda perlu membuang pengecualian seperti Microsoft.CSharp.RuntimeBinderException: 'NoSuchMember'
  • Tantangannya legal: maka lakukan saja tindakan yang diperlukan


ExpressionTree ini dibentuk selama pelaksanaan delegasi Pembaruan dan disimpan dalam Target . Target - L0 cache, kita akan berbicara lebih banyak tentang cache nanti.

Jadi, Target menyimpan ExpressionTree terakhir yang dihasilkan melalui delegasi Pembaruan . Mari kita lihat bagaimana aturan ini terlihat seperti contoh dari tipe Managed yang diteruskan ke metode 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: } } 


Blok paling penting bagi kami:

 .If ($$arg0 .TypeEqual ConsoleApp12.Foo && $$arg1 .TypeEqual System.Int32) 


$$ arg0 dan $$ arg1 adalah parameter yang disebut Target :
 Foo.o__0.p__0.Target((CallSite) Foo.o__0.p__0, <b>this</b>, <b>instance</b>); 


Diterjemahkan ke dalam manusia, ini berarti yang berikut:

Kami telah memverifikasi bahwa jika parameter pertama adalah tipe Foo dan yang kedua adalah Int32 , maka Anda dapat memanggil Boo ((objek) $$ arg1 dengan aman) .

 .Return #Label1 { .Block() { .Call $$arg0.Boo((System.Object)((System.Int32)$$arg1)); .Default(System.Object) } 


Catatan: jika terjadi kesalahan yang mengikat, blok Label1 terlihat seperti ini:
 .Return #Label1 { .Throw .New Microsoft.CSharp.RuntimeBinderException("NoSuchMember") 


Pemeriksaan ini disebut kendala . Ada dua jenis batasan : menurut jenis objek dan berdasarkan contoh spesifik objek (objek harus persis sama). Jika setidaknya salah satu batasan gagal, kami harus memeriksa kembali ekspresi dinamis untuk validitas, untuk ini kami akan memanggil delegasi Pembaruan . Menurut skema yang sudah kami ketahui, ia akan melakukan penjilidan dengan tipe baru dan menyimpan ExpressionTree baru di Target .

Cache



Kami telah menemukan bahwa Target adalah cache L0 . Setiap kali Target dipanggil, hal pertama yang akan kita lakukan adalah melewati batasan yang sudah tersimpan di dalamnya. Jika batasan gagal dan ikatan baru dihasilkan, maka aturan lama berjalan secara bersamaan ke L1 dan L2 . Di masa mendatang, ketika Anda melewatkan cache L0 , aturan dari L1 dan L2 akan dicari sampai ditemukan yang sesuai.

  • L1 : Sepuluh aturan terakhir yang telah meninggalkan L0 (disimpan langsung di CallSite )
  • L2 : 128 aturan terakhir yang dibuat menggunakan contoh binder spesifik (yaitu CallSiteBinder , unik untuk setiap CallSite )


Sekarang kita akhirnya dapat menambahkan detail ini menjadi satu kesatuan dan menjelaskan dalam bentuk algoritma apa yang terjadi ketika Foo.Bar (someDynamicObject) dipanggil :

1. Binder dibuat yang mengingat konteks dan metode yang dipanggil di tingkat tanda tangan mereka

2. Pertama kali operasi dipanggil, ExpressionTree dibuat, yang menyimpan:
2.1 Keterbatasan . Dalam hal ini, ini akan menjadi dua batasan pada jenis parameter pengikatan saat ini
2.2 Fungsi obyektif : melemparkan beberapa pengecualian (dalam hal ini tidak mungkin, karena dinamika apa pun akan berhasil mengarah ke objek) atau panggilan ke metode Bilah

3. Kompilasi dan jalankan ExpressionTree yang dihasilkan

4. Ketika Anda mengingat kembali operasi, dua opsi dimungkinkan:
4.1 Batasan berhasil : cukup panggil Bar
4.2 Batasan tidak berfungsi : ulangi langkah 2 untuk parameter pengikatan baru

Jadi, dengan contoh tipe Managed , menjadi semakin jelas bagaimana dinamika bekerja dari dalam. Dalam kasus yang dijelaskan, kami tidak akan pernah melewatkan cache, karena jenisnya selalu sama *, oleh karena itu Pembaruan akan dipanggil tepat sekali ketika CallSite diinisialisasi. Kemudian, untuk setiap panggilan, hanya batasan yang akan diperiksa dan fungsi tujuan akan dipanggil segera. Ini sesuai dengan pengamatan kami terhadap ingatan: tidak ada perhitungan - tidak ada kebocoran.

* Untuk alasan ini, kompiler menghasilkan CallSites untuk masing-masing: kemungkinan kehilangan cache L0 sangat berkurang

Saatnya untuk mengetahui bagaimana skema ini berbeda dalam kasus objek COM asli . Mari kita lihat 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: } } 


Dapat dilihat bahwa perbedaannya hanya pada batasan kedua:

 .If ($$arg0 .TypeEqual ConsoleApp12.Foo && .Block(System.Object $var1) { $var1 = .Constant<System.WeakReference>(System.WeakReference).Target; $var1 != null && (System.Object)$$arg1 == $var1 }) 


Jika dalam kasus kode Terkelola kami memiliki dua batasan pada jenis objek, maka di sini kita melihat bahwa pembatasan kedua memeriksa kesetaraan contoh melalui WeakReference .

Catatan: Pembatasan instance selain objek COM juga digunakan untuk TransparentProxy

Dalam praktiknya, berdasarkan pengetahuan kami tentang operasi cache, ini berarti bahwa setiap kali kami membuat kembali objek COM dalam satu loop, kami akan kehilangan cache L0 (dan L1 / L2 juga, karena aturan lama dengan tautan akan disimpan di sana untuk contoh lama). Asumsi pertama yang meminta Anda di kepala adalah bahwa cache aturan mengalir. Tetapi kode di sana cukup sederhana dan semuanya baik-baik saja di sana: aturan lama dihapus dengan benar. Pada saat yang sama, menggunakan WeakReference di ExpressionTree tidak menghalangi GC dari mengumpulkan objek yang tidak perlu.

Mekanisme untuk menyimpan aturan dalam 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; } 


Jadi apa masalahnya? Mari kita coba memperjelas hipotesis: kebocoran memori terjadi di suatu tempat ketika mengikat objek COM .

Eksperimen, bagian 2



Sekali lagi, mari kita beralih dari kesimpulan spekulatif ke eksperimen. Pertama, mari kita ulangi apa yang dilakukan kompiler untuk kita:

 //Simple COM object var comType = Type.GetTypeFromCLSID(new Guid("435356F9-F33F-403D-B475-1E4AB512FF95")); 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); while (true) { object instance = Activator.CreateInstance(comType); callSite.Target(callSite, this, instance); } 


Kami memeriksa:



Kebocoran itu telah dipertahankan. Adil. Tapi apa alasannya? Setelah mempelajari kode binder (yang kita tinggalkan di belakang kurung), jelas bahwa satu-satunya hal yang mempengaruhi jenis objek kita adalah opsi pembatasan. Mungkin ini bukan soal benda COM , tapi pengikat? Tidak ada banyak pilihan, mari kita memprovokasi pengikatan berganda untuk tipe yang Dikelola :

 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); } 




Wow! Sepertinya kita menangkapnya. Masalahnya tidak sama sekali dengan objek COM , seperti yang tampak bagi kita pada awalnya, hanya karena keterbatasan pada contoh, ini adalah satu-satunya kasus di mana pengikatan terjadi berkali-kali di dalam loop kita. Dalam semua kasus lain, saya mendapatkan cache L0 dan mengikatnya sekali.

Kesimpulan



Kebocoran memori



Jika Anda bekerja dengan variabel dinamis yang mengandung COM asli atau TransparentProxy , jangan pernah berikan mereka sebagai parameter metode. Jika Anda masih perlu melakukan ini, gunakan pemeran eksplisit ke objek dan kemudian kompiler akan tertinggal di belakang Anda

Salah :
 dynamic com = Activator.CreateInstance(comType); //do some work Marshal.FinalReleaseComObject(com); 


Dengan benar :
 dynamic com = Activator.CreateInstance(comType); //do some work Marshal.FinalReleaseComObject((object) com); 


Sebagai tindakan pencegahan tambahan, cobalah untuk instantiate objek seperti itu sesering mungkin. Sebenarnya untuk semua versi .NET Framework . (Untuk saat ini) tidak terlalu relevan untuk. Inti NET , karena tidak ada dukungan untuk objek COM dinamis .

Performa



Adalah kepentingan Anda bahwa kesalahan cache jarang terjadi, karena dalam hal ini tidak perlu menemukan aturan yang sesuai dalam cache tingkat tinggi. Kehilangan dalam cache L0 akan terjadi terutama dalam kasus ketidakcocokan jenis objek dinamis dengan pembatasan yang dipertahankan.

 dynamic com = GetSomeObject(); public object GetSomeObject() { //:      //:         } 


Namun, dalam praktiknya, Anda mungkin tidak akan melihat perbedaan dalam kinerja kecuali jika jumlah panggilan ke fungsi ini diukur dalam jutaan atau jika variabilitas jenis tidak terlalu besar. Biaya jika terjadi kesalahan pada cache L0 sedemikian rupa, N adalah jumlah jenis:

  • N <10. Jika Anda terlewatkan, lakukan saja aturan L1 cache yang ada
  • 10 < N <128 . Pencacahan L1 dan L2 cache (maksimum 10 dan N iterasi). Membuat dan mengisi array 10 elemen
  • N > 128. Iterasi lebih dari L1 dan L2 cache. Buat dan isi array 10 dan 128 elemen. Jika Anda melewatkan cache L2 , mengikat kembali


Dalam kasus kedua dan ketiga, beban pada GC akan meningkat.

Kesimpulan



Sayangnya, kami tidak menemukan alasan sebenarnya untuk kebocoran memori, ini akan membutuhkan studi terpisah dari pengikat tersebut. Untungnya, WinDbg memberikan petunjuk untuk penyelidikan lebih lanjut: sesuatu yang buruk terjadi di DLR . Kolom pertama adalah jumlah objek



Bonus



Mengapa casting ke objek secara eksplisit mencegah kebocoran?
Jenis apa pun dapat dilemparkan ke objek , sehingga operasi tidak lagi dinamis.

Mengapa tidak ada kebocoran saat bekerja dengan bidang dan metode objek COM?
Inilah yang tampak seperti ExpressionTree untuk akses bidang:

 .If ( .Call System.Dynamic.ComObject.IsComObject($$arg0) ) { .Return #Label1 { .Dynamic GetMember ComMarks(.Call System.Dynamic2.ComObject.ObjectToComObject($$arg0)) } } 

Source: https://habr.com/ru/post/id466657/


All Articles