Clojure tidak melakukan optimasi panggilan ekor sendiri: ketika Anda memiliki fungsi rekursif ekor dan Anda ingin mengoptimalkannya, Anda harus menggunakan formulir khusus recur
. Demikian pula, jika Anda memiliki dua fungsi yang saling rekursif, Anda dapat mengoptimalkannya hanya dengan menggunakan trampoline
.
Scala compiler dapat melakukan TCO untuk fungsi rekursif, tetapi tidak untuk dua fungsi rekursif yang saling menguntungkan.
Setiap kali saya membaca tentang keterbatasan ini, mereka selalu dianggap berasal dari batasan intrinsik dengan model JVM. Saya tidak tahu banyak tentang kompiler, tapi ini sedikit membingungkan saya. Biarkan saya ambil contohnya Programming Scala
. Di sini fungsinya
def approximate(guess: Double): Double =
if (isGoodEnough(guess)) guess
else approximate(improve(guess))
diterjemahkan ke dalam
0: aload_0
1: astore_3
2: aload_0
3: dload_1
4: invokevirtual #24; //Method isGoodEnough:(D)Z
7: ifeq
10: dload_1
11: dreturn
12: aload_0
13: dload_1
14: invokevirtual #27; //Method improve:(D)D
17: dstore_1
18: goto 2
Jadi, pada level bytecode, kita hanya perlu goto
. Dalam hal ini, sebenarnya, kerja keras dilakukan oleh kompiler.
Fasilitas apa dari mesin virtual yang mendasarinya yang memungkinkan kompiler menangani TCO dengan lebih mudah?
Sebagai catatan, saya tidak berharap mesin yang sebenarnya jauh lebih pintar daripada JVM. Namun, banyak bahasa yang mengkompilasi ke kode asli, seperti Haskell, tampaknya tidak memiliki masalah dengan mengoptimalkan panggilan ekor (well, Haskell kadang-kadang bisa karena kemalasan, tapi itu masalah lain).
tailcall
instruksi untuk JVM telah diusulkan sejak 2007: Blog di sun.com melalui mesin wayback . Setelah pengambilalihan Oracle, tautan ini 404-an. Saya kira itu tidak masuk ke daftar prioritas JVM 7.tailcall
instruksi hanya akan menandai panggilan ekor sebagai panggilan ekor. Apakah JVM kemudian benar-benar dioptimalkan mengatakan panggilan ekor adalah pertanyaan yang sama sekali berbeda. CLI CIL memiliki.tail
awalan instruksi, namun CLR Microsoft 64-bit untuk waktu yang lama tidak mengoptimalkannya. OTOH, IBM J9 JVM tidak mendeteksi panggilan ekor dan mengoptimalkan mereka, tanpa perlu instruksi khusus untuk menceritakannya yang panggilan panggilan ekor. Memberi keterangan tentang tail tail dan mengoptimalkan tail tail benar-benar orthogonal. (Terlepas dari kenyataan bahwa secara statis menyimpulkan panggilan mana yang merupakan panggilan ekor mungkin atau mungkin tidak dapat diputuskan. Tak tahu.)call something; oreturn
. Pekerjaan utama dari pembaruan spesifikasi JVM bukanlah untuk memperkenalkan instruksi panggilan-ekor yang eksplisit tetapi untuk mengamanatkan bahwa instruksi semacam itu dioptimalkan. Instruksi semacam itu hanya membuat pekerjaan penulis kompiler lebih mudah: Penulis JVM tidak harus memastikan untuk mengenali bahwa urutan instruksi sebelum menjadi hancur tak dapat dikenali, dan kompilator bytecode X-> dapat yakin bahwa bytecode mereka salah atau tidak sebenarnya dioptimalkan, tidak pernah benar-tetapi-tumpukan-meluap.call something; return;
hanya akan setara dengan panggilan ekor jika hal yang dipanggil tidak pernah meminta jejak stack; jika metode yang dimaksud adalah virtual atau memanggil metode virtual, JVM tidak akan memiliki cara untuk mengetahui apakah ia dapat menanyakan tentang stack.Clojure dapat melakukan optimasi otomatis rekursi ekor menjadi loop: tentu saja dimungkinkan untuk melakukan ini pada JVM saat Scala membuktikan.
Sebenarnya itu adalah keputusan desain untuk tidak melakukan ini - Anda harus secara eksplisit menggunakan
recur
formulir khusus jika Anda menginginkan fitur ini. Lihat utas surat Re: Mengapa tidak ada optimasi panggilan ekor di grup google Clojure.Pada JVM saat ini, satu-satunya hal yang tidak mungkin dilakukan adalah optimasi panggilan ekor antara fungsi yang berbeda (saling rekursi). Ini tidak terlalu rumit untuk diimplementasikan (bahasa lain seperti Skema telah memiliki fitur ini dari awal) tetapi akan membutuhkan perubahan pada spesifikasi JVM. Misalnya, Anda harus mengubah aturan tentang mempertahankan tumpukan panggilan fungsi yang lengkap.
Iterasi JVM di masa mendatang kemungkinan akan mendapatkan kemampuan ini, meskipun mungkin sebagai opsi agar perilaku yang kompatibel dengan mundur untuk kode lama tetap dipertahankan. Katakanlah, Fitur Pratinjau di Geeknizer mencantumkan ini untuk Java 9:
Tentu saja, roadmap masa depan selalu berubah.
Ternyata, itu bukan masalah besar. Dalam lebih dari 2 tahun coding Clojure, saya tidak pernah mengalami situasi di mana kurangnya TCO adalah sebuah masalah. Alasan utama untuk ini adalah:
recur
atau loop. Kasus rekursi timbal balik cukup langka dalam kode normalsumber
Ini bukan tentang menjadi lebih pintar, tetapi tentang menjadi berbeda. Sampai saat ini, JVM dirancang dan dioptimalkan secara eksklusif untuk satu bahasa (Java, tentu saja), yang memiliki memori yang sangat ketat dan model panggilan.
Tidak hanya tidak ada
goto
atau pointer, bahkan tidak ada cara untuk memanggil fungsi 'telanjang' (yang bukan metode yang didefinisikan dalam kelas).Secara konseptual, ketika menargetkan JVM, penulis kompiler harus bertanya "bagaimana saya bisa mengekspresikan konsep ini dalam istilah Java?". Dan jelas, tidak ada cara untuk mengekspresikan TCO di Jawa.
Perhatikan bahwa ini tidak dilihat sebagai kegagalan JVM, karena mereka tidak diperlukan untuk Java. Segera setelah Java membutuhkan beberapa fitur seperti ini, ia ditambahkan ke JVM.
Baru-baru ini pihak berwenang Jawa mulai menganggap serius JVM sebagai platform untuk bahasa non-Jawa, sehingga sudah mendapatkan beberapa dukungan untuk fitur yang tidak memiliki Java yang setara. Yang paling dikenal adalah pengetikan dinamis, yang sudah ada di JVM tetapi tidak di Java.
sumber
Pernahkah Anda memperhatikan bahwa alamat metode dimulai dengan 0? Bahwa semua metode set dimulai dengan 0? JVM tidak memungkinkan seseorang untuk melompat keluar dari suatu metode.
Saya tidak tahu apa yang akan terjadi dengan cabang dengan offset di luar metode dimuat oleh java - mungkin itu akan ditangkap oleh bytecode verifier, mungkin itu akan menghasilkan pengecualian, dan mungkin itu benar-benar akan melompat keluar dari metode.
Masalahnya, tentu saja, adalah bahwa Anda tidak dapat benar-benar menjamin di mana metode lain dari kelas yang sama akan menjadi, apalagi metode kelas lain. Saya ragu JVM membuat jaminan tentang di mana ia akan memuat metode, meskipun saya akan senang bisa diperbaiki.
sumber