Apakah Ruby melakukan Pengoptimalan Panggilan Tail?

92

Bahasa fungsional mengarah pada penggunaan rekursi untuk menyelesaikan banyak masalah, dan oleh karena itu banyak dari mereka melakukan Tail Call Optimization (TCO). TCO menyebabkan panggilan ke fungsi dari fungsi lain (atau fungsi itu sendiri, dalam hal ini fitur ini juga dikenal sebagai Tail Recursion Elimination, yang merupakan bagian dari TCO), sebagai langkah terakhir dari fungsi tersebut, agar tidak memerlukan bingkai tumpukan baru, yang mengurangi penggunaan overhead dan memori.

Ruby jelas telah "meminjam" sejumlah konsep dari bahasa fungsional (lambda, fungsi seperti map dan sebagainya, dll.), Yang membuat saya penasaran: Apakah Ruby melakukan optimasi panggilan ekor?

Charlie Flowers
sumber

Jawaban:

128

Tidak, Ruby tidak menjalankan TCO. Namun, itu juga tidak melakukan TCO.

Spesifikasi Bahasa Ruby tidak menjelaskan apapun tentang TCO. Ia tidak mengatakan Anda harus melakukannya, tetapi juga tidak mengatakan Anda tidak dapat melakukannya. Anda tidak bisa mengandalkannya .

Ini tidak seperti Skema, di mana Bahasa Spesifikasi mengharuskan bahwa semua Implementasi harus melakukan TCO. Tetapi ini juga tidak seperti Python, di mana Guido van Rossum telah membuatnya sangat jelas pada beberapa kesempatan (terakhir kali hanya beberapa hari yang lalu) bahwa Implementasi Python tidak boleh melakukan TCO.

Yukihiro Matsumoto bersimpati pada TCO, dia hanya tidak ingin memaksa semua Implementasi untuk mendukungnya. Sayangnya, ini berarti Anda tidak dapat mengandalkan TCO, atau jika Anda melakukannya, kode Anda tidak lagi dapat dibawa ke Implementasi Ruby lainnya.

Jadi, beberapa Implementasi Ruby melakukan TCO, tetapi sebagian besar tidak. YARV, misalnya, mendukung TCO, meskipun (untuk saat ini) Anda harus secara eksplisit menghapus komentar baris di kode sumber dan mengkompilasi ulang VM, untuk mengaktifkan TCO - di versi mendatang ini akan aktif secara default, setelah implementasi terbukti stabil. Mesin Virtual Parrot mendukung TCO secara native, oleh karena itu Cardinal dapat dengan mudah mendukungnya juga. CLR memiliki beberapa dukungan untuk TCO, yang berarti IronRuby dan Ruby.NET mungkin dapat melakukannya. Rubinius mungkin bisa melakukannya juga.

Tetapi JRuby dan XRuby tidak mendukung TCO, dan mereka mungkin tidak mendukung, kecuali JVM itu sendiri mendapatkan dukungan untuk TCO. Masalahnya adalah ini: jika Anda ingin memiliki implementasi yang cepat, dan integrasi yang cepat dan mulus dengan Java, maka Anda harus kompatibel-stack dengan Java dan menggunakan stack JVM sebanyak mungkin. Anda dapat dengan mudah mengimplementasikan TCO dengan trampolin atau gaya penerusan lanjutan eksplisit, tetapi kemudian Anda tidak lagi menggunakan tumpukan JVM, yang berarti bahwa setiap kali Anda ingin memanggil ke Java atau memanggil dari Java ke Ruby, Anda harus melakukan beberapa jenis konversi, yang lambat. Jadi, XRuby dan JRuby memilih untuk menggunakan kecepatan dan integrasi Java melalui TCO dan kelanjutan (yang pada dasarnya memiliki masalah yang sama).

Ini berlaku untuk semua implementasi Ruby yang ingin berintegrasi erat dengan beberapa platform host yang tidak mendukung TCO secara native. Misalnya, saya kira MacRuby akan mengalami masalah yang sama.

Jörg W Mittag
sumber
2
Saya mungkin salah (tolong beri tahu saya jika demikian), tetapi saya ragu TCO masuk akal dalam bahasa OO yang sebenarnya, karena panggilan ekor harus dapat menggunakan kembali bingkai tumpukan pemanggil. Karena dengan pengikatan akhir, tidak diketahui pada waktu kompilasi metode mana yang akan dipanggil oleh pengiriman pesan, tampaknya sulit untuk memastikannya (mungkin dengan JIT umpan balik tipe, atau dengan memaksa semua pelaksana pesan untuk menggunakan bingkai tumpukan dengan ukuran yang sama, atau dengan membatasi TCO untuk mengirim sendiri pesan yang sama…).
Damien Pollet
2
Itu respon yang bagus. Info itu tidak mudah ditemukan melalui Google. Menarik bahwa yarv mendukungnya.
Charlie Flowers
15
Damien, ternyata TCO sebenarnya diperlukan untuk bahasa OO yang sebenarnya: lihat projectfortress.sun.com/Projects/Community/blog/… . Jangan terlalu khawatir tentang hal-hal bingkai tumpukan: sangat mungkin untuk merancang bingkai tumpukan dengan bijaksana sehingga mereka bekerja dengan baik dengan TCO.
Tony Garnock-Jones
2
tonyg menyimpan postingan referensi GLS dari kepunahan, mencerminkannya di sini: eighty-twenty.org/index.cgi/tech/oo-tail-calls-20111001.html
Frank Shearar
Saya melakukan tugas pekerjaan rumah yang mengharuskan saya membongkar satu set array bersarang dengan kedalaman yang berubah-ubah. Cara yang jelas untuk melakukannya adalah secara rekursif, dan kasus penggunaan serupa online (yang dapat saya temukan) menggunakan rekursi. Masalah khusus saya sangat tidak mungkin meledak, bahkan tanpa TCO, tetapi fakta bahwa saya tidak dapat menulis solusi yang sepenuhnya umum tanpa beralih ke iterasi mengganggu saya.
Isaac Rabinovitch
42

Pembaruan: Berikut penjelasan bagus tentang TCO di Ruby: http://nithinbekal.com/posts/ruby-tco/

Pembaruan: Anda mungkin juga ingin melihat permata tco_method : http://blog.tdg5.com/introducing-the-tco_method-gem/

Di Ruby MRI (1.9, 2.0 dan 2.1) Anda dapat mengaktifkan TCO dengan:

RubyVM::InstructionSequence.compile_option = {
  :tailcall_optimization => true,
  :trace_instruction => false
}

Ada usulan untuk mengaktifkan TCO secara default di Ruby 2.0. Ini juga menjelaskan beberapa masalah yang menyertainya: Pengoptimalan panggilan tail: aktifkan secara default ?.

Kutipan singkat dari tautan:

Umumnya, pengoptimalan rekursi-ekor mencakup teknik pengoptimalan lain - terjemahan "panggil" untuk "melompat". Menurut saya, sulit untuk menerapkan optimasi ini karena mengenali "rekursi" sulit di dunia Ruby.

Contoh selanjutnya. fact () pemanggilan metode dalam klausa "else" bukanlah "panggilan ekor".

def fact(n) 
  if n < 2
    1 
 else
   n * fact(n-1) 
 end 
end

Jika Anda ingin menggunakan metode tail-call optimization on fact (), Anda perlu mengubah metode fact () sebagai berikut (gaya penerusan lanjutan).

def fact(n, r) 
  if n < 2 
    r
  else
    fact(n-1, n*r)
  end
end
Ernest
sumber
12

Itu dapat memiliki tetapi tidak dijamin untuk:

https://bugs.ruby-lang.org/issues/1256

Steve Jessop
sumber
Tautan sudah mati sampai sekarang.
karatedog
@ karatedog: terima kasih, diperbarui. Meskipun sejujurnya referensinya mungkin sudah ketinggalan zaman, karena bug tersebut sekarang berusia 5 tahun dan ada aktivitas pada subjek yang sama sejak saat itu.
Steve Jessop
Ya :-) Saya baru saja membaca tentang topik ini dan saya melihat bahwa di Ruby 2.0 itu dapat diaktifkan dari kode sumber (tidak ada lagi modifikasi sumber C dan kompilasi ulang).
karatedog
4

TCO juga dapat dikompilasi dengan mengubah beberapa variabel di vm_opts.h sebelum mengkompilasi: https://github.com/ruby/ruby/blob/trunk/vm_opts.h#L21

// vm_opts.h
#define OPT_TRACE_INSTRUCTION        0    // default 1
#define OPT_TAILCALL_OPTIMIZATION    1    // default 0
Christopher Kuttruff
sumber
2

Ini dibangun di atas jawaban Jörg dan Ernest. Pada dasarnya itu tergantung pada implementasi.

Saya tidak bisa mendapatkan jawaban Ernest untuk mengerjakan MRI, tetapi itu bisa dilakukan. Saya menemukan contoh ini yang berfungsi untuk MRI 1.9 hingga 2.1. Ini akan mencetak angka yang sangat besar. Jika Anda tidak menyetel opsi TCO ke true, Anda akan mendapatkan kesalahan "tumpukan terlalu dalam".

source = <<-SOURCE
def fact n, acc = 1
  if n.zero?
    acc
  else
    fact n - 1, acc * n
  end
end

fact 10000
SOURCE

i_seq = RubyVM::InstructionSequence.new source, nil, nil, nil,
  tailcall_optimization: true, trace_instruction: false

#puts i_seq.disasm

begin
  value = i_seq.eval

  p value
rescue SystemStackError => e
  p e
end
Kelvin
sumber