Pada artikel ini, dasar-dasar perangkat tipe internal akan diberikan, serta contoh di mana memori untuk tipe referensi akan dialokasikan sepenuhnya pada stack (ini karena saya adalah 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 memulai cerita, saya sangat menyarankan Anda membaca posting pertama tentang
StructLayout , karena ada contoh yang dianalisis yang akan digunakan dalam artikel ini (Namun, seperti biasa).
Latar belakang
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 frekuensi yang dikatakan orang bahwa tipe referensi berbeda dari yang signifikan karena yang pertama terletak di heap dan yang kedua di stack, saya memutuskan untuk menggunakan assembler untuk menunjukkan bahwa tipe referensi dapat hidup di stack. Namun, saya mulai menghadapi semua jenis masalah, misalnya, mengembalikan alamat yang diinginkan dan menyatakannya sebagai tautan terkelola (saya masih mengerjakannya). Jadi saya mulai menipu dan melakukan apa yang tidak berfungsi di assembler, di C #. Dan pada akhirnya, assembler tidak tetap sama sekali.
Juga, rekomendasi untuk membaca - jika Anda terbiasa dengan perangkat jenis referensi, saya sarankan melewatkan teori tentang mereka (hanya dasar-dasar yang akan diberikan, tidak ada yang menarik).
Sedikit tentang struktur internal tipe
Saya ingin mengingatkan Anda bahwa pemisahan memori pada tumpukan dan tumpukan terjadi pada tingkat .NET, dan pembagian ini murni logis, secara fisik tidak ada perbedaan antara area memori di bawah tumpukan dan di bawah tumpukan. Perbedaan produktivitas sudah disediakan secara khusus dengan bekerja dengan bidang-bidang ini.
Lalu bagaimana cara mengalokasikan memori pada stack? Untuk mulai dengan, mari kita lihat bagaimana tipe referensi misterius ini terstruktur dan apa yang ada di dalamnya, yang tidak signifikan.
Jadi, pertimbangkan contoh paling sederhana dengan kelas Karyawan.
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 lihat bagaimana disajikan dalam memori.
UPD: Kelas ini dipertimbangkan pada contoh sistem 32-bit.

Jadi, selain memori untuk bidang, kami memiliki dua bidang tersembunyi lagi - indeks blok sinkronisasi (kata judul objek dalam gambar) dan alamat tabel metode.
Bidang pertama, ini adalah indeks dari blok sinkronisasi, kami tidak terlalu tertarik. Saat menempatkan tipenya, saya memutuskan untuk menghilangkannya. Saya melakukan ini karena dua alasan:
- Saya sangat malas (saya tidak mengatakan bahwa alasannya masuk akal)
- Bidang ini opsional untuk fungsi dasar objek
Tetapi karena kita sudah 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 ini. Blok itu sendiri terletak di tabel blok sinkronisasi (a la global array). 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, kebetulan, tidak dimiliki oleh struktur, tumpukan raja). Asumsikan bahwa kelas Karyawan juga mengimplementasikan tiga antarmuka: IComparable, IDisposable, dan ICloneable.
Maka tabel metode akan terlihat seperti ini

Gambarnya sangat keren, di sana, pada prinsipnya, semuanya dicat dan dimengerti. Jika pendek di jari, maka metode virtual dipanggil tidak langsung di alamat, tetapi dengan offset di tabel metode. Dalam hierarki, metode virtual yang sama akan ditempatkan pada offset yang sama dalam tabel metode. Yaitu, kita memanggil metode pada kelas dasar pada offset, tidak mengetahui tipe tabel metode mana yang akan digunakan, tetapi mengetahui bahwa pada offset ini akan ada metode yang paling relevan untuk tipe runtime.
Perlu juga diingat bahwa referensi ke objek menunjuk ke tabel metode.
Contoh yang sudah lama 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 pointer paling sederhana untuk jenis yang dikelola dan sebaliknya. Sangat mudah untuk mendapatkan pointer dari tautan terkelola, tetapi transformasi terbalik menyebabkan saya kesulitan, dan tanpa berpikir dua kali, saya menerapkan atribut favorit saya. Untuk menjaga kode dalam satu kunci, saya melakukannya dalam 2 arah dalam satu cara.
Kode di sini // public class PointerCasterFacade { public virtual unsafe T GetManagedReferenceByPointer<T>(int* pointer) => default(T); public virtual unsafe int* GetPointerByManagedReference<T>(T managedReference) => (int*)0; } // 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, tulis metode yang membawa pointer ke beberapa memori (tidak harus di stack, by the way) dan mengkonfigurasi tipe.
Untuk memudahkan 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 untuk mengoptimalkan kode ini, itu lebih menarik bagi saya untuk membuatnya dimengerti. Selanjutnya, menggunakan konverter yang dijelaskan sebelumnya, kita mendapatkan pointer ke tipe yang dibuat.
Pointer ini menunjuk tepat ke tabel metode. Oleh karena itu, cukup dengan hanya mendapatkan konten dari memori yang ditunjuknya. Ini akan menjadi alamat tabel metode.
Dan karena pointer yang diberikan kepada kita adalah semacam referensi ke objek, kita harus menuliskan alamat tabel metode tepat di mana ia menunjuk.
Itu saja, sebenarnya. Tanpa diduga, kan? Sekarang tipe kita sudah siap. Pinocchio, yang mengalokasikan kami memori, akan mengurus inisialisasi bidang.
Tetap hanya menggunakan grandcaster 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 oleh semua hukum jenis referensi (well, hampir) terletak sebuah objek yang dibangun dari tanah dan tongkat hitam. 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. Tidak ada pembicaraan tentang panggilan ke metode virtual; mari kita terbang dengan pengecualian. Metode reguler dipanggil langsung, dalam kode hanya akan ada alamat untuk metode nyata, sehingga mereka akan bekerja. Dan di tempat ladang akan ... tetapi tidak ada yang tahu apa yang akan ada di sana.
Karena tidak mungkin menggunakan metode terpisah untuk inisialisasi pada stack (karena frame stack akan dihapus setelah kembali dari metode), memori harus dialokasikan oleh metode yang ingin menggunakan jenis pada stack. Sebenarnya, tidak ada satu cara untuk melakukan ini. Tapi yang paling cocok untuk kita adalah stackalloc. Hanya kata kunci yang sempurna untuk keperluan kita. Sayangnya, dialah yang memperkenalkan ketidakteraturan ke dalam kode. Sebelum itu, ada ide untuk menggunakan Span untuk tujuan ini dan untuk melakukannya tanpa kode yang tidak aman. Tidak ada yang salah dengan kode yang tidak aman, tetapi seperti di tempat lain, itu bukan peluru perak dan memiliki area aplikasi sendiri.
Selanjutnya, 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 menggunakan ini dalam proyek nyata, metode yang mengalokasikan memori pada tumpukan menggunakan T () baru, yang pada gilirannya menggunakan refleksi untuk membuat tipe pada heap! Jadi metode ini akan lebih lambat dari penciptaan yang biasa dari jenis sekali, yah, 40-50.
Di sini Anda dapat melihat keseluruhan proyek.
Sumber: dalam penyimpangan teoretis, contoh digunakan dari buku Sasha Goldstein - Pro .NET Performace