Ruby có thực hiện Tối ưu hóa cuộc gọi đuôi không?


92

Các ngôn ngữ hàm dẫn đến việc sử dụng đệ quy để giải quyết rất nhiều vấn đề và do đó nhiều trong số chúng thực hiện Tối ưu hóa cuộc gọi đuôi (TCO). TCO gây ra các lệnh gọi đến một hàm từ một hàm khác (hoặc chính nó, trong trường hợp đó, tính năng này còn được gọi là Loại bỏ đệ quy đuôi, là một tập hợp con của TCO), là bước cuối cùng của hàm đó, để không cần một khung ngăn xếp mới, làm giảm chi phí sử dụng và bộ nhớ.

Ruby rõ ràng đã "vay mượn" một số khái niệm từ các ngôn ngữ chức năng (lambdas, các chức năng như bản đồ, v.v.), điều này khiến tôi tò mò: Ruby có thực hiện tối ưu hóa lệnh gọi đuôi không?

Câu trả lời:


127

Không, Ruby không thực hiện TCO. Tuy nhiên, nó cũng không thực hiện TCO.

Đặc tả ngôn ngữ Ruby không nói gì về TCO. Nó không nói rằng bạn phải làm, nhưng nó cũng không nói rằng bạn không thể làm được. Bạn chỉ không thể dựa vào nó.

Đây là không giống như Đề án, nơi mà các kỹ thuật ngôn ngữ đòi hỏi rằng tất cả Triển khai phải thực hiện TCO. Nhưng nó cũng không giống như Python, nơi mà Guido van Rossum đã nói rất rõ ràng trong nhiều lần (lần gần đây nhất chỉ vài ngày trước) rằng Triển khai Python không nên thực hiện TCO.

Yukihiro Matsumoto có thiện cảm với TCO, anh ấy chỉ không muốn buộc tất cả các Triển khai phải hỗ trợ nó. Thật không may, điều này có nghĩa là bạn không thể dựa vào TCO, hoặc nếu bạn làm vậy, mã của bạn sẽ không còn khả dụng cho các Triển khai Ruby khác.

Vì vậy, một số Triển khai Ruby thực hiện TCO, nhưng hầu hết thì không. YARV, ví dụ, hỗ trợ TCO, mặc dù (hiện tại) bạn phải bỏ ghi chú rõ ràng một dòng trong mã nguồn và biên dịch lại VM, để kích hoạt TCO - trong các phiên bản tương lai, nó sẽ được bật theo mặc định, sau khi triển khai chứng minh ổn định. Máy ảo Parrot hỗ trợ TCO nguyên bản, do đó Cardinal cũng có thể dễ dàng hỗ trợ nó. CLR có một số hỗ trợ cho TCO, có nghĩa là IronRuby và Ruby.NET có thể làm được điều đó. Rubinius cũng có thể làm được.

Nhưng JRuby và XRuby không hỗ trợ TCO và có thể họ sẽ không hỗ trợ, trừ khi bản thân JVM nhận được hỗ trợ cho TCO. Vấn đề là ở đây: nếu bạn muốn triển khai nhanh, tích hợp nhanh và liền mạch với Java, thì bạn nên tương thích với Java và sử dụng ngăn xếp của JVM càng nhiều càng tốt. Bạn có thể khá dễ dàng triển khai TCO với trampolines hoặc kiểu truyền tiếp tục rõ ràng, nhưng sau đó bạn không còn sử dụng ngăn xếp JVM nữa, có nghĩa là mỗi khi bạn muốn gọi vào Java hoặc gọi từ Java vào Ruby, bạn phải thực hiện một số loại chuyển đổi chậm. Vì vậy, XRuby và JRuby đã chọn đi với tốc độ và tích hợp Java qua TCO và tính liên tục (về cơ bản có cùng một vấn đề).

Điều này áp dụng cho tất cả các triển khai của Ruby muốn tích hợp chặt chẽ với một số nền tảng máy chủ không hỗ trợ TCO nguyên bản. Ví dụ, tôi đoán MacRuby sẽ gặp vấn đề tương tự.


2
Tôi có thể nhầm lẫn (xin hãy thông cảm cho tôi nếu vậy), nhưng tôi nghi ngờ TCO có ý nghĩa gì trong các ngôn ngữ OO thực sự, vì lệnh gọi đuôi phải có thể sử dụng lại khung ngăn xếp người gọi. Vì với tính năng liên kết muộn, không biết phương thức nào sẽ được gọi vào thời điểm biên dịch khi gửi thư, có vẻ như khó đảm bảo điều đó (có thể với JIT phản hồi kiểu hoặc bằng cách buộc tất cả người triển khai thư sử dụng khung ngăn xếp có cùng kích thước hoặc bằng cách hạn chế TCO tự gửi cùng một tin nhắn…).
Damien Pollet

2
Đó là một phản hồi tuyệt vời. Thông tin đó không dễ dàng tìm thấy qua Google. Thật thú vị khi yarv hỗ trợ nó.
Charlie Flowers

15
Damien, hóa ra TCO thực sự được yêu cầu đối với các ngôn ngữ OO thực sự: xem projectfortress.sun.com/Projects/Community/blog/… . Đừng lo lắng quá nhiều về nội dung khung xếp chồng: hoàn toàn có thể thiết kế khung xếp chồng một cách hợp lý để chúng hoạt động tốt với TCO.
Tony Garnock-Jones

2
tonyg lưu GLS' bài tham chiếu từ cơ tuyệt chủng, phản ánh nó ở đây: eighty-twenty.org/index.cgi/tech/oo-tail-calls-20111001.html
Frank Shearar

Tôi đang làm một bài tập về nhà yêu cầu tôi phải tháo rời một tập hợp các mảng lồng nhau có độ sâu tùy ý. Cách rõ ràng để làm điều đó là đệ quy, và các trường hợp sử dụng tương tự trực tuyến (mà tôi có thể tìm thấy) sử dụng đệ quy. Vấn đề cụ thể của tôi rất khó xảy ra, ngay cả khi không có TCO, nhưng thực tế là tôi không thể viết một giải pháp hoàn toàn chung chung mà không chuyển sang lặp lại làm phiền tôi.
Isaac Rabinovitch

42

Cập nhật: Đây là lời giải thích hay về TCO trong Ruby: http://nithinbekal.com/posts/ruby-tco/

Cập nhật: Bạn cũng có thể muốn xem đá quý tco_method : http://blog.tdg5.com/introductioning-the-tco_method-gem/

Trong Ruby MRI (1.9, 2.0 và 2.1), bạn có thể bật TCO với:

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

Có một đề xuất để bật TCO theo mặc định trong Ruby 2.0. Nó cũng giải thích một số vấn đề đi kèm với đó là: Tối ưu hóa cuộc gọi đuôi: bật theo mặc định ?.

Trích đoạn ngắn từ liên kết:

Nói chung, tối ưu hóa đệ quy đuôi bao gồm một kỹ thuật tối ưu hóa khác - dịch "gọi" thành "nhảy". Theo tôi, rất khó để áp dụng tối ưu hóa này vì nhận ra "đệ quy" rất khó trong thế giới của Ruby.

Ví dụ tiếp theo. Lệnh gọi phương thức fact () trong mệnh đề "else" không phải là "lệnh gọi đuôi".

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

Nếu bạn muốn sử dụng tối ưu hóa cuộc gọi đuôi trên phương thức fact (), bạn cần thay đổi phương thức fact () như sau (kiểu truyền tiếp tục).

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

12

Nó có thể có nhưng không được đảm bảo:

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


Liên kết đã chết cho đến bây giờ.
karatedog

@karatedog: cảm ơn, đã cập nhật. Mặc dù thành thật mà nói, tài liệu tham khảo có thể đã lỗi thời, vì lỗi này đã được 5 năm tuổi và đã có hoạt động về cùng một chủ đề kể từ đó.
Steve Jessop

Có :-) Tôi vừa đọc về chủ đề và tôi thấy rằng trong Ruby 2.0, nó có thể được kích hoạt từ mã nguồn (không cần sửa đổi và biên dịch lại mã nguồn C nữa).
karatedog


2

Điều này dựa trên câu trả lời của Jörg và Ernest. Về cơ bản nó phụ thuộc vào việc thực hiện.

Tôi không thể nhận được câu trả lời của Ernest để làm việc trên MRI, nhưng nó có thể làm được. Tôi tìm thấy ví dụ này hoạt động cho MRI 1.9 đến 2.1. Điều này sẽ in một số lượng rất lớn. Nếu bạn không đặt tùy chọn TCO thành true, bạn sẽ gặp lỗi "ngăn xếp quá sâu".

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
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.