Artikel ini akan menunjukkan kepada Anda dasar-dasar tipe internal, seperti tentu saja contoh di mana memori untuk tipe referensi akan dialokasikan sepenuhnya pada stack (ini karena saya seorang programmer full-stack).

Penafian
Artikel ini tidak mengandung bahan yang harus digunakan dalam proyek nyata. Ini hanyalah perpanjangan dari batas-batas di mana bahasa pemrograman dirasakan.
Sebelum melanjutkan cerita, saya sangat menyarankan Anda untuk membaca posting pertama tentang
StructLayout , karena ada contoh yang akan digunakan dalam artikel ini (Namun, seperti biasa).
Prasejarah
Mulai menulis kode untuk artikel ini, saya ingin melakukan sesuatu yang menarik menggunakan bahasa assembly. Saya ingin entah bagaimana mematahkan model eksekusi standar dan mendapatkan hasil yang benar-benar tidak biasa. Dan mengingat seberapa sering orang mengatakan bahwa tipe referensi berbeda dari tipe nilai karena yang pertama terletak di heap dan yang kedua ada di tumpukan, saya memutuskan untuk menggunakan assembler untuk menunjukkan bahwa tipe referensi dapat hidup di tumpukan. Namun, saya mulai mengalami berbagai masalah, misalnya, mengembalikan alamat dan presentasinya sebagai tautan terkelola (saya masih mengerjakannya). Jadi saya mulai menipu dan melakukan sesuatu yang tidak berfungsi dalam bahasa assembly, di C #. Dan pada akhirnya, tidak ada assembler sama sekali.
Baca juga rekomendasi - jika Anda terbiasa dengan tata letak jenis referensi, saya sarankan melewatkan teori tentang mereka (hanya dasar-dasar yang akan diberikan, tidak ada yang menarik).
Sedikit tentang tipe internal (untuk kerangka lama, sekarang beberapa offset diubah, tetapi skema keseluruhannya sama)
Saya ingin mengingatkan bahwa pembagian memori menjadi tumpukan dan tumpukan terjadi di tingkat .NET, dan pembagian ini murni logis; secara fisik tidak ada perbedaan antara area memori di bawah tumpukan dan tumpukan. Perbedaan produktivitas disediakan hanya oleh algoritma yang berbeda untuk bekerja dengan kedua bidang ini.
Lalu, bagaimana cara mengalokasikan memori pada stack? Untuk memulainya, mari kita memahami bagaimana tipe referensi misterius ini diatur dan apa yang dimilikinya, tipe nilai itu tidak memiliki.
Jadi, perhatikan contoh paling sederhana dengan karyawan kelas.
Kode Karyawanpublic class Employee { private int _id; private string _name; public virtual void Work() { Console.WriteLine(“Zzzz...”); } public void TakeVacation(int days) { Console.WriteLine(“Zzzz...”); } public static void SetCompanyPolicy(CompanyPolicy policy) { Console.WriteLine("Zzzz..."); } }
Dan mari kita lihat bagaimana disajikan dalam memori.
Kelas ini dipertimbangkan pada contoh sistem 32-bit.

Dengan demikian, selain memori untuk bidang, kami memiliki dua bidang tersembunyi lagi - indeks blok sinkronisasi (judul kata header objek pada gambar) dan alamat tabel metode.
Bidang pertama (indeks blok sinkronisasi) tidak terlalu menarik bagi kami. Ketika menempatkan jenis saya memutuskan untuk melewati itu. Saya melakukan ini karena dua alasan:
- Saya sangat malas (saya tidak mengatakan bahwa alasannya masuk akal)
- Untuk operasi dasar objek, bidang ini tidak diperlukan.
Tetapi karena kita sudah mulai berbicara, saya pikir benar untuk mengatakan beberapa kata tentang bidang ini. Ini digunakan untuk berbagai tujuan (kode hash, sinkronisasi). Sebaliknya, bidang itu sendiri hanyalah indeks dari salah satu blok sinkronisasi yang terkait dengan objek yang diberikan. Blok sendiri terletak di tabel blok sinkronisasi (sesuatu seperti array global). Membuat blok semacam itu adalah operasi yang agak besar, jadi itu tidak dibuat jika tidak diperlukan. Selain itu, ketika menggunakan kunci tipis, pengenal utas yang menerima kunci (bukan indeks) akan ditulis di sana.
Bidang kedua jauh lebih penting bagi kami. Berkat tabel metode jenis, alat yang ampuh seperti polimorfisme dimungkinkan (yang, omong-omong, struktur, tumpukan raja, tidak miliki).
Misalkan kelas Karyawan juga mengimplementasikan tiga antarmuka: IComparable, IDisposable, dan ICloneable.
Maka tabel metode akan terlihat seperti ini.

Gambarnya sangat keren, semuanya ditampilkan dan semuanya jelas. Singkatnya, metode virtual tidak dipanggil langsung berdasarkan alamat, tetapi oleh offset dalam tabel metode. Dalam hierarki, metode virtual yang sama akan ditempatkan pada offset yang sama dalam tabel metode. Yaitu, pada kelas dasar kita memanggil metode dengan offset, tidak mengetahui jenis tabel metode yang akan digunakan, tetapi mengetahui bahwa offset ini akan menjadi metode yang paling relevan untuk jenis runtime.
Juga perlu diingat bahwa referensi objek menunjuk hanya ke pointer tabel metode.
Contoh yang ditunggu-tunggu
Mari kita mulai dengan kelas yang akan membantu kita dalam tujuan kita. Menggunakan StructLayout (saya benar-benar mencoba tanpa itu, tetapi tidak berhasil), saya menulis pemetaan sederhana - pointer ke jenis yang dikelola dan kembali. Mendapatkan pointer dari tautan terkelola cukup mudah, tetapi transformasi terbalik menyebabkan saya kesulitan dan, tanpa berpikir dua kali, saya menerapkan atribut favorit saya. Untuk menyimpan kode dalam satu kunci, dibuat dalam 2 arah dalam satu cara.
Kode pembuat peta // Provides the signatures we need public class PointerCasterFacade { public virtual unsafe T GetManagedReferenceByPointer<T>(int* pointer) => default(T); public virtual unsafe int* GetPointerByManagedReference<T>(T managedReference) => (int*)0; } // Provides the logic we need public class PointerCasterUnderground { public virtual T GetManagedReferenceByPointer<T>(T reference) => reference; public virtual unsafe int* GetPointerByManagedReference<T>(int* pointer) => pointer; } [StructLayout(LayoutKind.Explicit)] public class PointerCaster { public PointerCaster() { pointerCaster= new PointerCasterUnderground(); } [FieldOffset(0)] private PointerCasterUnderground pointerCaster; [FieldOffset(0)] public PointerCasterFacade Caster; }
Pertama, kita menulis metode yang membawa pointer ke beberapa memori (tidak harus di stack, by the way) dan mengkonfigurasi tipe.
Untuk kesederhanaan menemukan alamat tabel metode, saya membuat tipe di heap. Saya yakin bahwa tabel metode dapat ditemukan dengan cara lain, tetapi saya tidak menetapkan diri saya tujuan mengoptimalkan kode ini, itu lebih menarik bagi saya untuk membuatnya dimengerti. Selanjutnya, menggunakan konverter yang dijelaskan sebelumnya, kami memperoleh pointer ke tipe yang dibuat.
Pointer ini menunjuk tepat ke tabel metode. Oleh karena itu, cukup dengan hanya memperoleh konten dari memori yang ditunjuknya. Ini akan menjadi alamat tabel metode.
Dan karena pointer yang diberikan kepada kita adalah semacam referensi objek, kita juga harus menulis alamat tabel metode tepat di mana ia menunjuk.
Sebenarnya itu saja. Tiba-tiba, bukan? Sekarang tipe kita sudah siap. Pinocchio, yang mengalokasikan memori kepada kami, akan mengurus inisialisasi ladang sendiri.
Tetap hanya menggunakan ultra-mega caster kami untuk mengubah pointer menjadi tautan terkelola.
public class StackInitializer { public static unsafe T InitializeOnStack<T>(int* pointer) where T : new() { T r = new T(); var caster = new PointerCaster().Caster; int* ptr = caster.GetPointerByManagedReference(r); pointer[0] = ptr[0]; T reference = caster.GetManagedReferenceByPointer<T>(pointer); return reference; } }
Sekarang kita memiliki tautan pada tumpukan yang menunjuk ke tumpukan yang sama, di mana menurut semua hukum jenis referensi (well, hampir) terletak sebuah objek yang dibangun dari tanah hitam dan tongkat. Polimorfisme tersedia.
Harus dipahami bahwa jika Anda melewati tautan ini di luar metode, maka setelah kembali dari itu, kami akan mendapatkan sesuatu yang tidak jelas. Tentang panggilan metode virtual dan ucapan tidak bisa, pengecualian akan terjadi. Metode normal dipanggil langsung, kode hanya akan memiliki alamat untuk metode nyata, sehingga mereka akan berfungsi. Dan di tempat ladang akan ... dan tidak ada yang tahu apa yang akan ada di sana.
Karena tidak mungkin menggunakan metode terpisah untuk inisialisasi pada stack (karena frame stack akan ditimpa setelah kembali dari metode), metode yang ingin menerapkan jenis pada stack harus mengalokasikan memori. Sebenarnya, ada beberapa cara untuk melakukannya. Tapi yang paling cocok untuk kita adalah
stackalloc . Hanya kata kunci yang sempurna untuk keperluan kita. Sayangnya, ini membawa kode yang
tidak aman . Sebelum itu, ada ide untuk menggunakan Span untuk tujuan ini dan untuk melakukannya tanpa kode yang tidak aman. Dalam kode yang tidak aman tidak ada yang buruk, tetapi seperti di mana-mana, itu bukan peluru perak dan memiliki area aplikasi sendiri.
Kemudian, setelah menerima pointer ke memori pada stack saat ini, kami meneruskan pointer ini ke metode yang membentuk tipe di bagian. Itu semua yang mendengarkan - dilakukan dengan baik.
unsafe class Program { public static void Main() { int* pointer = stackalloc int[2]; var a = StackInitializer.InitializeOnStack<StackReferenceType>(pointer); a.StubMethod(); Console.WriteLine(a.Field); Console.WriteLine(a); Console.Read(); } }
Anda seharusnya tidak menggunakannya dalam proyek nyata, metode mengalokasikan memori pada stack menggunakan T () baru, yang pada gilirannya menggunakan refleksi untuk membuat tipe pada heap! Jadi metode ini akan lebih lambat dari biasanya penciptaan jenis kali, di 40-50. Apalagi itu bukan lintas platform.
Di sini Anda dapat menemukan seluruh proyek.
Sumber: dalam panduan teoretis, contoh-contoh dari buku Sasha Goldstein - Pro .NET Performace digunakan