UPDATE: Saya sangat menyukai pertanyaan ini sehingga menjadikannya subjek blog saya pada 18 November 2011 . Terima kasih atas pertanyaannya!
Saya selalu bertanya-tanya: apa tujuan tumpukan itu?
Saya berasumsi Anda maksud tumpukan evaluasi bahasa MSIL, dan bukan tumpukan per-thread yang sebenarnya saat runtime.
Mengapa ada transfer dari memori ke stack atau "loading?" Di sisi lain, mengapa ada transfer dari stack ke memori atau "penyimpanan"? Mengapa tidak menempatkan semuanya dalam memori saja?
MSIL adalah bahasa "mesin virtual". Kompiler seperti kompiler C # menghasilkan CIL , dan kemudian pada saat runtime kompiler lain yang disebut kompiler JIT (Just In Time) mengubah IL menjadi kode mesin aktual yang dapat dieksekusi.
Jadi pertama-tama mari kita jawab pertanyaan "mengapa memiliki MSIL?" Mengapa tidak hanya membuat kompiler C # menulis kode mesin?
Karena lebih murah melakukannya dengan cara ini. Misalkan kita tidak melakukannya dengan cara itu; misalkan setiap bahasa harus memiliki generator kode mesin sendiri. Anda memiliki dua puluh bahasa berbeda: C #, JScript .NET , Visual Basic, IronPython , F # ... Dan anggaplah Anda memiliki sepuluh prosesor yang berbeda. Berapa banyak pembuat kode yang harus Anda tulis? 20 x 10 = 200 generator kode. Itu banyak pekerjaan. Sekarang anggaplah Anda ingin menambahkan prosesor baru. Anda harus menulis pembuat kode untuk itu dua puluh kali, satu untuk setiap bahasa.
Lebih jauh lagi, ini adalah pekerjaan yang sulit dan berbahaya. Menulis generator kode yang efisien untuk chip yang bukan keahlian Anda adalah pekerjaan berat! Desainer kompiler ahli dalam analisis semantik bahasa mereka, bukan pada alokasi register efisien dari set chip baru.
Sekarang anggaplah kita melakukannya dengan cara CIL. Berapa banyak generator CIL yang harus Anda tulis? Satu per bahasa. Berapa banyak kompiler JIT yang harus Anda tulis? Satu per prosesor. Total: 20 + 10 = 30 generator kode. Selain itu, generator bahasa-ke-CIL mudah untuk ditulis karena CIL adalah bahasa yang sederhana, dan generator kode-CIL ke mesin juga mudah ditulis karena CIL adalah bahasa yang sederhana. Kami menyingkirkan semua seluk-beluk C # dan VB dan yang lainnya dan "menurunkan" semuanya ke bahasa sederhana yang mudah untuk menulis jitter.
Memiliki bahasa perantara menurunkan biaya memproduksi kompiler bahasa baru secara dramatis . Ini juga menurunkan biaya mendukung chip baru secara dramatis. Anda ingin mendukung chip baru, Anda menemukan beberapa ahli pada chip itu dan minta mereka menulis jitter CIL dan Anda selesai; Anda kemudian mendukung semua bahasa tersebut di chip Anda.
OK, jadi kami telah menetapkan mengapa kami memiliki MSIL; karena memiliki bahasa perantara menurunkan biaya. Lalu mengapa bahasa itu "mesin tumpukan"?
Karena mesin stack secara konseptual sangat sederhana untuk penulis kompiler bahasa untuk menangani. Stack adalah mekanisme sederhana dan mudah dipahami untuk menggambarkan komputasi. Mesin stack juga secara konsep sangat mudah untuk penulis kompiler JIT untuk berurusan dengan. Menggunakan tumpukan adalah abstraksi yang disederhanakan, dan karena itu sekali lagi, ini menurunkan biaya kami .
Anda bertanya "mengapa punya setumpuk sama sekali?" Mengapa tidak melakukan semuanya langsung dari memori? Baiklah, mari kita pikirkan tentang itu. Misalkan Anda ingin membuat kode CIL untuk:
int x = A() + B() + C() + 10;
Misalkan kita memiliki konvensi yang "menambahkan", "panggilan", "toko" dan seterusnya selalu mengambil argumen mereka dari tumpukan dan meletakkan hasilnya (jika ada) di tumpukan. Untuk menghasilkan kode CIL untuk C # ini kami hanya mengatakan sesuatu seperti:
load the address of x // The stack now contains address of x
call A() // The stack contains address of x and result of A()
call B() // Address of x, result of A(), result of B()
add // Address of x, result of A() + B()
call C() // Address of x, result of A() + B(), result of C()
add // Address of x, result of A() + B() + C()
load 10 // Address of x, result of A() + B() + C(), 10
add // Address of x, result of A() + B() + C() + 10
store in address // The result is now stored in x, and the stack is empty.
Sekarang misalkan kita melakukannya tanpa tumpukan. Kami akan melakukannya dengan cara Anda, di mana setiap opcode mengambil alamat operandnya dan alamat di mana ia menyimpan hasilnya :
Allocate temporary store T1 for result of A()
Call A() with the address of T1
Allocate temporary store T2 for result of B()
Call B() with the address of T2
Allocate temporary store T3 for the result of the first addition
Add contents of T1 to T2, then store the result into the address of T3
Allocate temporary store T4 for the result of C()
Call C() with the address of T4
Allocate temporary store T5 for result of the second addition
...
Anda lihat bagaimana ini terjadi? Kode kita menjadi semakin besar karena kita harus secara eksplisit mengalokasikan semua penyimpanan sementara yang biasanya dengan konvensi tinggal ditumpuk . Lebih buruk lagi, opcode kita sendiri menjadi semakin besar karena mereka semua sekarang harus mengambil sebagai argumen alamat yang akan mereka tulis hasilnya, dan alamat masing-masing operan. Instruksi "add" yang tahu bahwa ia akan mengambil dua hal dari stack dan meletakkan satu hal dapat berupa satu byte tunggal. Instruksi tambahan yang mengambil dua alamat operan dan alamat hasil akan sangat besar.
Kami menggunakan opcode berbasis tumpukan karena tumpukan memecahkan masalah umum . Yaitu: Saya ingin mengalokasikan beberapa penyimpanan sementara, gunakan segera dan kemudian singkirkan segera setelah saya selesai . Dengan membuat asumsi bahwa kita memiliki tumpukan yang kita miliki, kita dapat membuat opcode sangat kecil dan kodenya sangat singkat.
PEMBARUAN: Beberapa pemikiran tambahan
Kebetulan, ide ini menurunkan biaya secara drastis dengan (1) menetapkan mesin virtual, (2) menulis kompiler yang menargetkan bahasa VM, dan (3) menulis implementasi VM pada berbagai perangkat keras, bukan ide baru sama sekali . Itu tidak berasal dari MSIL, LLVM, bytecode Java, atau infrastruktur modern lainnya. Implementasi paling awal dari strategi ini yang saya ketahui adalah mesin pcode dari tahun 1966.
Yang pertama saya secara pribadi mendengar konsep ini adalah ketika saya belajar bagaimana implementor Infocom berhasil membuat Zork berjalan pada begitu banyak mesin yang berbeda dengan sangat baik. Mereka menentukan mesin virtual yang disebut mesin- Z dan kemudian membuat emulator mesin-Z untuk semua perangkat keras yang mereka inginkan untuk menjalankan game mereka. Ini memiliki manfaat tambahan yang sangat besar bahwa mereka dapat menerapkan manajemen memori virtual pada sistem 8-bit primitif; sebuah gim bisa lebih besar daripada yang bisa masuk ke dalam memori karena mereka bisa saja memasukkan kode dari disk ketika mereka membutuhkannya dan membuangnya ketika mereka perlu memuat kode baru.