Runtime, Just-in-Time, Bahasa “Hybrid” (?)
Dari Twatternya @lynxluna, di refer ke gue:
Gue coba jawab yah:
Apa itu Runtime
Runtime dan Just-in-Time sebenarnya nggak setara. Runtime itu adalah komponen library yang diperlukan agar suatu aplikasi bisa jalan di suatu environment. Contoh gampang: libc itu runtime default aplikasi yang jalan di atas Linux. Kalo di Windows, ada VC++ runtime yang suka diinstall sama game-game atau aplikasi. JRE (Java Runtime Environment) itu runtime untuk program-programnya Java, .NET Runtime buat program-program .NET, Python juga punya runtime sendiri, begitupula Rust. Kecenderungannya, setiap bahasa pemrograman memiliki runtime masing-masing yang perlu untuk juga terinstall apabila aplikasi yang ditulis dalam bahasa tersebut ingin dijalankan di suatu mesin.
Bahasa “Hybrid” dan Compiler
Dalam pertanyaan di atas, penanya menyebutkan bahasa “hybrid”. Sebenarnya nggak ada terminologi bahasa “hybrid” dalam bahasa pemrograman. Namun gue paham maksud bahasa “hybrid” dalam konteks tersebut adalah bahasa-bahasa yang tidak dikompilasi langsung ke bahasa mesin, karena penanya menyebutkan Just-in-Time (JIT) dan interpreter. Jadi, konteks penanya pasti ada di seputaran bahasa-bahasa semacam Java, C#, Python, JavaScript, dan bahasa apapun yang tidak dikompilasi ke bahasa mesin secara langsung (termasuk juga C dan C++ jika dikompilasi ke WebAssembly (WASM)!).
Sebenarnya kalau bicara soal bahasa pemrograman, compiler itu adalah program yang melakukan penerjemahan dari suatu bahasa pemrograman ke bahasa pemrograman lainnya. Jadi, ada bahasa sumber (source) dan bahasa tujuan (target). Bahasa tujuannya itu bisa apapun sebenarnya. Nggak serta merta harus selalu bahasa mesin. Compiler Java mengompilasi program Java menjadi Java bytecode agar bisa dieksekusi oleh Java Virtual Machine (JVM) dalam JRE. Compiler C#, Roslyn, mengompilasi C# (atau VB.NET) ke Microsoft Intermediate Language (MSIL) agar bisa dieksekusi oleh .NET Runtime. Bahkan, seperti yang gue sebutkan sebelumnya, C dan C++ juga bisa dikompilasi ke WASM untuk dapat dijalankan oleh runtime WASM dalam JavaScript/WASM engine seperti V8, SpiderMonkey, atau WASMTime.
Secara teknis, bahasa target dari kompiler ya juga adalah bahasa pemrograman. Bahasa Mesin (Assembly), Java Bytecode, MSIL, WASM, semuanya juga bahasa pemrograman. Kita juga bisa menulis program langsung dalam bahasa-bahasa tersebut, cuman ya lebih susah, karena bahasa-bahasa tersebut abstraksinya lebih sedikit dibandingkan bahasa-bahasa pemrograman tingkat tinggi (high-level).
Interpreter vs. Compiler
Sebelumnya gue menyebutkan program-program yang dikompilasi langsung ke bahasa mesin atau sering disebut native. Program native bisa langsung dieksekusi oleh mesin/komputer karena bahasanya memang dikenali langsung oleh prosesor. Dengan kata lain, bahasa target dari kompilasi tersebut adalah instruksi-instruksi mesin yang dikenal langsung oleh mesin yang menjalankannya. Namun bagaimana dengan Java bytecode, MSIL, WASM, dan bahasa-bahasa lainnya yang “kelihatannya tidak di-compile“?
Program-program yang tidak dikompilasi langsung ke bahasa mesin harus diterjemahkan dulu ke bahasa mesin native pada saat runtime. Oleh karenanya, runtime dari program-program tersebut juga menyertakan interpreter dan/atau compiler agar bisa mengeksekusi program-program yang ditulis dengan bahasa tersebut.
Ada dua strategi yang dilakukan oleh PL engineer dalam membuat suatu runtime dari bahasa pemrograman yang tidak di-compile ke native. Strategi pertama adalah menggunakan interpreter. Interpreter adalah program yang akan mengeksekusi secara langsung baris demi baris program sumbernya (source). Suatu interpreter akan memiliki representasi abstraksi sesuai dengan fasilitas-fasilitas yang disediakan oleh bahasa pemrogramannya. Kelebihannya adalah program-program yang menggunakan interpreter tidak perlu dikompilasi ke mesin yang berbeda-beda berkali-kali jika ingin dijalankan di mesin yang berbeda, atau portabel. Tinggal gunakan interpreter yang bersesuaian saja, misalnya buat mesin Intel (x86), ARM (AArch64), dll.
Tapi, interpreter akan jauh lebih lambat dibandingkan program-program yang dieksekusi secara native karena abstraks-abstraksi high-level tersebut tidak diterjemahkan ke representasi yang dimengerti langsung oleh mesin. Contohnya, dalam interpreter, suatu variabel akan dialokasikan berdasarkan namanya secara langsung, dan diindeks menggunakan suatu tabel. Ketika ada instruksi yang merujuk ke variabel tersebut, interpreter akan mencari dalam tabel berdasarkan namanya untuk mengaksesnya. Proses mencari variabel berdasarkan nama itu akan memakan waktu yang jauh lebih lama dibandingkan dengan langsung menunjuk ke suatu alamat memory spesifik, seperti yang dilakukan oleh program-program yang dikompilasi langsung ke bahasa mesin. Selain itu, kelemahan besar dari interpreter adalah dia cenderung tidak bisa memprediksi apa instruksi yang perlu dieksekusi setelahnya, jadi eksekusi satu instruksi yang lama akan menyebabkan stall dan menambahkan waktu eksekusi. Padahal kalau di mesin native, ada namanya speculative execution, yang mana mesin akan mengeksekusi instruksi setelahnya tanpa menunggu instruksi sebelumnya selesai. Ini akan memberikan performance gain yang signifikan.
Nah, bagaimana untuk menangani kekurangan dari interpreter itu, sembari mempertahankan kelebihannya sebagai bahasa pemrograman dan sistem yang portabel? Diperkenalkanlah Just-in-Time (JIT) Compilation! JIT adalah teknik melakukan kompilasi dari program pada saat program tersebut akan dieksekusi. Berbeda dengan kompilasi konvensional yang dilakukan jauh sebelum program akan dijalankan, JIT memungkinkan proses kompilasi ke bahasa target pada saat dia dijalankan.
Kelebihan dari JIT adalah dia memiliki performa yang jauh lebih tinggi dibandingkan teknik intepreter. Karena JIT menghasilkan bahasa target berupa bahasa mesin di mana program tersebut dijalankan. Hasil kompilasi dari JIT adalah instruksi native yang dapat langsung dieksekusi oleh prosesor. Oleh karenanya, banyak bahasa pemrograman scripting seperti JavaScript, PHP, dan Python juga sudah menyertakan JIT di dalam runtimenya. Selain itu, sistem-sistem dalam bahasa pemrograman yang menggunakan bahasa intermediate (Java bytecode, MSIL, WASM), juga menggunakan teknik kompilasi JIT untuk mengeksekusi programnya.
Kekurangan dari teknik JIT adalah warm-up time dari JIT jauh lebih tinggi dibandingkan program-program yang dikompilasi langsung ke bahasa native. Warm-up time adalah waktu yang diperlukan oleh runtime untuk melakukan kompilasi bagian dari aplikasi hingga aplikasi tersebut dijalankan. Selain itu, ketika program telah selesai dan proses berakhir, hasil kompilasi sebelumnya akan terbuang dan tidak dapat digunakan apabila program yang sama dijalankan kembali. Ketika program yang sama dijalankan kembali, runtime perlu melakukan kembali warm-up dari kode-kode yang sama, sehingga proses kompilasi terjadi setiap kali program akan dijalankan.
Untuk mengatasi salah satu kekurangan tersebut, maka diperkenalkan lagi alternatif dari teknik JIT, yaitu Ahead-of-Time (AOT) Compilation. AOT adalah teknik yang bisa dibilang hampir serupa seperti kompilasi konvensional yang memproduksi bahasa native. Bedanya adalah hasil proses AOT tidak bisa berdiri sendiri tanpa runtime, walaupun kini beberapa sistem runtime sebenarnya ada yang memungkinkan, seperti .NET Native atau Mono embedded. AOT juga diperkenalkan pada sistem Android ketika migrasi dari Dalvik ke AndroidRT pada Android versi 4 dulu. Dan ini juga menjadi alasan kenapa proses instalasi aplikasi di Android sangat lama, karena Android melakukan AOT dari aplikasinya pada saat proses instalasi.
Perkembangan bahasa pemrograman saat ini membuat compiler menjadi bagian yang tidak terpisahkan dari runtime itu sendiri. Teknik kompilasi konvensional, JIT, dan AOT semuanya sama-sama menggunakan compiler, namun posisi proses kompilasinya saja yang berbeda dalam pipeline suatu program dari kode sumber hingga kode yang bisa dieksekusi oleh mesin.