Foreplay
Pertimbangkan kode berikut:
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:
Semuanya baik saat ini:

Mari kembali ke versi asli kode, tetapi ubah jenis objek:
Dan lagi, tidak ada kejutan:

Mari kita coba opsi ketiga:
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:
- Membuat instance objek COM asli dengan sendirinya tidak menyebabkan kebocoran - GC berhasil mengatasi dengan membersihkan memori
- Saat bekerja dengan kelas Managed apa pun, kebocoran tidak terjadi
- 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 cepatC # 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();
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);
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:
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:
- Mengikat operasi dinamis ke operasi statis (mekanisme pengikatan berada di luar cakupan artikel ini)
- 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:
- Jenis yang disebut metode ini
- Jenis objek yang dilewatkan oleh parameter (untuk memastikan bahwa itu dapat dilemparkan ke jenis parameter)
- 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
Bilah3. Kompilasi dan jalankan ExpressionTree yang dihasilkan
4. Ketika Anda mengingat kembali operasi, dua opsi dimungkinkan:
4.1
Batasan berhasil : cukup panggil
Bar4.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 berkurangSaatnya 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 TransparentProxyDalam 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:
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);
Dengan benar :
dynamic com = Activator.CreateInstance(comType);
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)) } }