Jawaban:
Ya, kompilasi ke bytecode Java lebih mudah daripada kompilasi ke kode mesin. Ini sebagian karena hanya ada satu format untuk ditargetkan (seperti Mandrill menyebutkan, meskipun ini hanya mengurangi kompleksitas kompiler, bukan waktu kompilasi), sebagian karena JVM adalah mesin yang jauh lebih sederhana dan lebih nyaman untuk diprogram daripada CPU nyata - seperti yang telah dirancang dalam bersama dengan bahasa Java, sebagian besar operasi Java memetakan tepat satu operasi bytecode dengan cara yang sangat sederhana. Alasan lain yang sangat penting adalah bahwa praktis tidakoptimasi terjadi. Hampir semua masalah efisiensi diserahkan kepada kompiler JIT (atau ke JVM secara keseluruhan), sehingga seluruh ujung tengah dari kompiler normal menghilang. Itu pada dasarnya dapat berjalan melalui AST sekali dan menghasilkan urutan bytecode siap pakai untuk setiap node. Ada beberapa "overhead administrasi" untuk menghasilkan tabel metode, kumpulan konstan, dll. Tapi itu tidak seberapa dibandingkan dengan kompleksitas, katakanlah, LLVM.
Kompiler hanyalah sebuah program yang mengambil 1 file teks yang dapat dibaca manusia dan menerjemahkannya menjadi instruksi biner untuk mesin. Jika Anda mundur selangkah dan memikirkan pertanyaan Anda dari perspektif teoretis ini, kerumitannya kira-kira sama. Namun, pada tingkat yang lebih praktis, kompiler kode byte lebih sederhana.
Apa langkah luas yang harus terjadi untuk mengkompilasi program?
Hanya ada dua perbedaan nyata antara keduanya.
Secara umum, sebuah program dengan beberapa unit kompilasi memerlukan penghubungan saat mengkompilasi ke kode mesin dan umumnya tidak dengan kode byte. Seseorang dapat memisahkan rambut tentang apakah menghubungkan adalah bagian dari kompilasi dalam konteks pertanyaan ini. Jika demikian, kompilasi kode byte akan sedikit lebih sederhana. Namun, kompleksitas penautan dibuat saat run-time ketika banyak masalah penautan ditangani oleh VM (lihat catatan saya di bawah).
Kompiler kode byte cenderung untuk tidak mengoptimalkan sebanyak karena VM dapat melakukan ini dengan lebih baik saat ini (kompiler JIT adalah tambahan yang cukup standar untuk VM saat ini).
Dari sini saya menyimpulkan bahwa kompiler kode byte dapat menghilangkan kompleksitas sebagian besar optimisasi dan semua penautan, menunda keduanya ke runtime VM. Kompiler kode byte lebih sederhana dalam prakteknya karena mereka menyekop banyak kerumitan ke VM yang diambil oleh kompiler kode mesin.
1 Tidak termasuk bahasa esoterik
Saya akan mengatakan bahwa menyederhanakan desain kompiler karena kompilasi selalu Java ke kode mesin virtual generik. Itu juga berarti Anda hanya perlu mengkompilasi kode sekali dan itu akan berjalan pada plataform apa pun (daripada harus mengkompilasi pada setiap mesin). Saya tidak begitu yakin apakah waktu kompilasi akan lebih rendah karena Anda dapat mempertimbangkan mesin virtual seperti mesin standar.
Di sisi lain, setiap mesin harus memiliki Java Virtual Machine dimuat sehingga dapat mengartikan "kode byte" (yang merupakan kode mesin virtual yang dihasilkan dari kompilasi kode java), menerjemahkannya ke kode mesin aktual dan menjalankannya .
Imo ini bagus untuk program yang sangat besar tetapi sangat buruk untuk yang kecil (karena mesin virtual adalah pemborosan memori).
Kompleksitas kompilasi sangat tergantung pada kesenjangan semantik antara bahasa sumber dan bahasa target dan tingkat optimisasi yang ingin Anda terapkan saat menjembatani kesenjangan ini.
Sebagai contoh, kompilasi kode sumber Java ke kode byte JVM relatif lurus ke depan, karena ada subset inti Jawa yang memetakan secara langsung ke subset kode byte JVM. Ada beberapa perbedaan: Java memiliki loop tetapi tidak GOTO
, JVM memiliki GOTO
tetapi tidak ada loop, Java memiliki generik, JVM tidak, tetapi mereka dapat dengan mudah ditangani (transformasi dari loop ke lompatan bersyarat adalah sepele, jenis penghapusan sedikit kurang jadi, tapi masih bisa dikelola). Ada perbedaan lain tetapi tidak terlalu parah.
Mengkompilasi kode sumber Ruby ke kode byte JVM jauh lebih banyak terlibat (terutama sebelum invokedynamic
dan MethodHandles
diperkenalkan di Java 7, atau lebih tepatnya di Edisi ke 3 dari spesifikasi JVM). Di Ruby, metode dapat diganti saat runtime. Pada JVM, unit kode terkecil yang dapat diganti saat runtime adalah kelas, jadi metode Ruby harus dikompilasi bukan untuk metode JVM tetapi untuk kelas JVM. Pengiriman metode Ruby tidak cocok dengan pengiriman metode JVM dan sebelumnya invokedynamic
, tidak ada cara untuk menyuntikkan mekanisme pengiriman metode Anda sendiri ke JVM. Ruby memiliki kelanjutan dan coroutine, tetapi JVM tidak memiliki fasilitas untuk mengimplementasikannya. (JVMGOTO
dibatasi untuk melompati target dalam metode ini.) Satu-satunya aliran kendali yang dimiliki JVM, yang akan cukup kuat untuk menerapkan kelanjutan adalah pengecualian dan untuk mengimplementasikan benang coroutine, keduanya sangat kelas berat, sedangkan seluruh tujuan coroutine adalah untuk menjadi sangat ringan.
OTOH, kompilasi kode sumber Ruby ke kode byte Rubinius atau kode byte YARV sekali lagi sepele, karena keduanya secara eksplisit dirancang sebagai target kompilasi untuk Ruby (walaupun Rubinius juga telah digunakan untuk bahasa lain seperti CoffeeScript, dan yang paling terkenal Fancy) .
Demikian juga, mengkompilasi kode asli x86 ke kode byte JVM tidak lurus ke depan, sekali lagi, ada celah semantik yang cukup besar.
Haskell adalah contoh lain yang baik: dengan Haskell, ada beberapa kompiler siap pakai yang berkinerja tinggi dengan kekuatan industri yang menghasilkan kode mesin x86 asli, tetapi sampai saat ini, tidak ada kompiler yang berfungsi baik untuk JVM atau CLI, karena semantik celahnya sangat besar sehingga sangat rumit untuk menjembataninya. Jadi, ini adalah contoh di mana kompilasi ke kode mesin asli sebenarnya kurang kompleks daripada kompilasi ke kode byte JVM atau CIL. Ini karena kode mesin asli memiliki primitif level jauh lebih rendah ( GOTO
, pointer, ...) yang dapat lebih mudah "dipaksa" untuk melakukan apa yang Anda inginkan daripada menggunakan primitif level yang lebih tinggi seperti pemanggilan metode atau pengecualian.
Jadi, dapat dikatakan bahwa semakin tinggi level bahasa targetnya, semakin dekat pula dengan bahasa semantik bahasa sumbernya untuk mengurangi kompleksitas kompiler.
Dalam praktiknya, sebagian besar JVM saat ini adalah perangkat lunak yang sangat kompleks, melakukan kompilasi JIT (sehingga bytecode diterjemahkan secara dinamis ke kode mesin oleh JVM).
Jadi sementara kompilasi dari kode sumber Java (atau kode sumber Clojure) ke kode byte JVM memang lebih sederhana, JVM itu sendiri sedang melakukan terjemahan kompleks ke kode mesin.
Fakta bahwa terjemahan JIT di dalam JVM ini dinamis memungkinkan JVM untuk fokus pada bagian-bagian yang paling relevan dari bytecode. Secara praktis, sebagian besar JVM mengoptimalkan lebih banyak bagian terpanas (misalnya metode yang paling disebut, atau blok dasar yang paling dieksekusi) dari bytecode JVM.
Saya tidak yakin kerumitan gabungan dari JVM + Java untuk bytecode compiler secara signifikan lebih kecil dari kompleksitas kompiler yang ada sebelumnya.
Perhatikan juga bahwa kebanyakan kompiler tradisional (seperti GCC atau Dentang / LLVM ) mentransformasikan kode sumber input C (atau C ++, atau Ada, ...) menjadi representasi internal ( Gimple untuk GCC, LLVM untuk Dentang) yang sangat mirip dengan beberapa bytecode. Kemudian mereka mentransformasikan representasi internal tersebut (pertama mengoptimalkannya menjadi dirinya sendiri, yaitu sebagian besar optimisasi GCC mengambil Gimple sebagai input dan memproduksi Gimple sebagai output; kemudian memancarkan assembler atau kode mesin darinya) ke kode objek.
BTW, dengan GCC baru-baru ini (terutama libgccjit ) dan infrastruktur LLVM, Anda dapat menggunakannya untuk mengkompilasi beberapa bahasa lain (atau Anda sendiri) ke dalam representasi Gimple atau LLVM internal mereka, kemudian dapatkan keuntungan dari banyak kemampuan optimisasi mid-end & back- bagian akhir dari kompiler ini.