Ruby ทำการเพิ่มประสิทธิภาพการโทรหางหรือไม่


92

ภาษาที่ใช้งานได้นำไปสู่การใช้การเรียกซ้ำเพื่อแก้ปัญหาจำนวนมากดังนั้นหลายภาษาจึงใช้ Tail Call Optimization (TCO) TCO ทำให้เกิดการเรียกใช้ฟังก์ชันจากฟังก์ชันอื่น (หรือตัวเองซึ่งในกรณีนี้คุณลักษณะนี้เรียกอีกอย่างว่า Tail Recursion Elimination ซึ่งเป็นส่วนย่อยของ TCO) เป็นขั้นตอนสุดท้ายของฟังก์ชันนั้นโดยไม่ต้องใช้สแต็กเฟรมใหม่ ซึ่งลดค่าใช้จ่ายและการใช้หน่วยความจำ

เห็นได้ชัดว่า Ruby ได้ "ยืม" แนวคิดหลายอย่างจากภาษาที่ใช้งานได้ (lambdas, ฟังก์ชันเช่น map เป็นต้น) ซึ่งทำให้ฉันสงสัยว่า Ruby ทำการเพิ่มประสิทธิภาพการโทรหางหรือไม่?

คำตอบ:


128

ไม่ Ruby ไม่ดำเนินการ TCO อย่างไรก็ตามยังไม่ได้ทำ TCO

ข้อกำหนดภาษา Ruby ไม่ได้บอกอะไรเกี่ยวกับ TCO มันไม่ได้บอกว่าคุณต้องทำมัน แต่มันก็ยังไม่ได้บอกว่าคุณไม่สามารถทำมันได้ คุณไม่สามารถพึ่งพามันได้

ซึ่งแตกต่างจาก Scheme ตรงที่ข้อกำหนดของภาษากำหนดให้การใช้งานทั้งหมดต้องดำเนินการ TCO แต่มันก็แตกต่างจาก Python ตรงที่ Guido van Rossum ได้กล่าวไว้อย่างชัดเจนในหลาย ๆ ครั้ง (ครั้งล่าสุดเมื่อสองสามวันก่อน) ว่า Python Implementations ไม่ควรทำ TCO

Yukihiro Matsumoto เห็นใจ TCO เขาไม่ต้องการบังคับให้Implementation ทั้งหมดสนับสนุน น่าเสียดายนั่นหมายความว่าคุณไม่สามารถพึ่งพา TCO ได้หรือหากคุณทำเช่นนั้นโค้ดของคุณจะไม่สามารถพกพาไปยังการใช้งาน Ruby อื่น ๆ ได้อีกต่อไป

ดังนั้นการใช้งาน Ruby บางส่วนจึงดำเนินการ TCO แต่ส่วนใหญ่ไม่ทำเช่นนั้น ตัวอย่างเช่น YARV รองรับ TCO แม้ว่า (ในขณะนี้) คุณต้องยกเลิกการใส่ข้อความในซอร์สโค้ดอย่างชัดเจนและคอมไพล์ VM ใหม่เพื่อเปิดใช้งาน TCO - ในเวอร์ชันต่อ ๆ ไปจะมีการเปิดใช้งานตามค่าเริ่มต้นหลังจากการใช้งานพิสูจน์แล้ว มั่นคง Parrot Virtual Machine รองรับ TCO โดยกำเนิดดังนั้น Cardinal จึงสามารถรองรับได้อย่างง่ายดายเช่นกัน CLR มีการสนับสนุน TCO ซึ่งหมายความว่า IronRuby และ Ruby.NET อาจทำได้ Rubinius ก็อาจทำได้เช่นกัน

แต่ JRuby และ XRuby ไม่รองรับ TCO และอาจจะไม่ได้เว้นแต่ JVM จะได้รับการสนับสนุน TCO ปัญหาคือ: หากคุณต้องการใช้งานที่รวดเร็วและการรวมเข้ากับ Java ที่รวดเร็วและราบรื่นคุณควรเข้ากันได้กับ Java แบบ stack และใช้สแต็กของ JVM ให้มากที่สุด คุณสามารถใช้ TCO กับแทรมโพลีนหรือรูปแบบการส่งผ่านต่อเนื่องอย่างชัดเจนได้อย่างง่ายดาย แต่คุณไม่ได้ใช้สแต็ก JVM อีกต่อไปซึ่งหมายความว่าทุกครั้งที่คุณต้องการเรียกเข้า Java หรือเรียกจาก Java เป็น Ruby คุณต้องดำเนินการบางอย่าง การแปลงซึ่งช้า ดังนั้น XRuby และ JRuby จึงเลือกใช้ความเร็วและการรวม Java ผ่าน TCO และความต่อเนื่อง (ซึ่งโดยทั่วไปมีปัญหาเดียวกัน)

สิ่งนี้ใช้กับการใช้งาน Ruby ทั้งหมดที่ต้องการผสานรวมอย่างแน่นหนากับแพลตฟอร์มโฮสต์บางตัวที่ไม่รองรับ TCO ตัวอย่างเช่นฉันเดาว่า MacRuby จะมีปัญหาเดียวกัน


2
ฉันอาจเข้าใจผิด (โปรดแจ้งให้ฉันทราบหากเป็นเช่นนั้น) แต่ฉันสงสัยว่า TCO มีความหมายในภาษา OO จริงเนื่องจากการเรียกหางต้องสามารถนำสแต็กเฟรมของผู้โทรกลับมาใช้ใหม่ได้ เนื่องจากมีการเชื่อมโยงล่าช้าจึงไม่ทราบว่าวิธีการใดที่จะถูกเรียกใช้โดยการส่งข้อความจึงยากที่จะตรวจสอบให้แน่ใจว่า (อาจจะใช้ JIT ประเภทป้อนกลับคำติชมหรือโดยการบังคับให้ผู้ใช้งานข้อความทั้งหมดใช้สแต็กเฟรม ที่มีขนาดเท่ากันหรือ จำกัด TCO ให้ส่งข้อความเดียวกันด้วยตนเอง…)
Damien Pollet

2
นั่นเป็นการตอบสนองที่ยอดเยี่ยม ข้อมูลนั้นหาไม่ได้ง่ายๆผ่านทาง Google น่าสนใจที่ yarv สนับสนุน
Charlie Flowers

15
ดาเมียนปรากฎว่า TCO เป็นจริงที่จำเป็นสำหรับภาษา OO จริง: ดูprojectfortress.sun.com/Projects/Community/blog/... อย่ากังวลกับเรื่องของสแต็กเฟรมมากเกินไป: เป็นไปได้อย่างสมบูรณ์แบบในการออกแบบสแต็กเฟรมอย่างสมเหตุสมผลเพื่อให้ทำงานร่วมกับ TCO ได้ดี
Tony Garnock-Jones

2
tonyg บันทึกโพสต์อ้างอิงของ GLS จากการสูญพันธุ์โดยสะท้อนที่นี่: eighty-twenty.org/index.cgi/tech/oo-tail-calls-20111001.html
Frank Shearar

ฉันกำลังทำการบ้านที่ต้องการให้ฉันถอดชุดอาร์เรย์ที่ซ้อนกันซึ่งมีความลึกตามอำเภอใจ วิธีที่ชัดเจนในการทำคือการเรียกซ้ำและกรณีการใช้งานที่คล้ายกันทางออนไลน์ (ที่ฉันสามารถหาได้) ใช้การเรียกซ้ำ ปัญหาเฉพาะของฉันไม่น่าจะระเบิดได้แม้จะไม่มี TCO แต่ความจริงที่ว่าฉันไม่สามารถเขียนวิธีแก้ปัญหาทั่วไปโดยไม่เปลี่ยนไปใช้การวนซ้ำทำให้ฉันรำคาญ
Isaac Rabinovitch

42

อัปเดต:นี่คือคำอธิบายที่ดีของ TCO ใน Ruby: http://nithinbekal.com/posts/ruby-tco/

อัปเดต:คุณอาจต้องการตรวจสอบอัญมณีtco_method : http://blog.tdg5.com/introducing-the-tco_method-gem/

ใน Ruby MRI (1.9, 2.0 และ 2.1) คุณสามารถเปิด TCO ได้ด้วย:

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

มีข้อเสนอให้เปิด TCO โดยค่าเริ่มต้นใน Ruby 2.0 นอกจากนี้ยังอธิบายปัญหาบางอย่างที่มาพร้อมกับสิ่งนั้น: Tail call optimization: enable by default ?.

ข้อความที่ตัดตอนมาสั้น ๆ จากลิงค์:

โดยทั่วไปการเพิ่มประสิทธิภาพการเรียกซ้ำหางจะมีเทคนิคการเพิ่มประสิทธิภาพอื่น ๆ นั่นคือการแปล "เรียก" เป็น "กระโดด" ในความคิดของฉันมันเป็นเรื่องยากที่จะใช้การเพิ่มประสิทธิภาพนี้เนื่องจากการรับรู้ "การเรียกซ้ำ" เป็นเรื่องยากในโลกของรูบี้

ตัวอย่างถัดไป fact () การเรียกใช้เมธอดในประโยค "else" ไม่ใช่ "tail call"

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

หากคุณต้องการใช้ tail-call optimization บนวิธี fact () คุณต้องเปลี่ยนวิธี fact () ดังต่อไปนี้ (รูปแบบการส่งต่อแบบต่อเนื่อง)

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

12

สามารถมีได้ แต่ไม่รับประกันว่าจะ:

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


ลิงก์ถูกใช้งานแล้ว ณ ตอนนี้
karatedog

@karatedog: ขอบคุณอัปเดต แม้ว่าตามจริงแล้วการอ้างอิงอาจล้าสมัยเนื่องจากบั๊กมีอายุ 5 ปีและมีกิจกรรมในเรื่องเดียวกันตั้งแต่นั้นมา
Steve Jessop

ใช่ :-) ฉันเพิ่งอ่านเกี่ยวกับหัวข้อนี้และฉันเห็นว่าใน Ruby 2.0 สามารถเปิดใช้งานจากซอร์สโค้ด (ไม่มีการแก้ไขซอร์ส C และคอมไพล์ใหม่อีกต่อไป)
karatedog


2

สิ่งนี้สร้างจากคำตอบของJörgและ Ernest โดยพื้นฐานแล้วขึ้นอยู่กับการนำไปใช้งาน

ฉันไม่สามารถรับคำตอบของ Ernest ในการทำงานกับ MRI ได้ แต่ก็ทำได้ ฉันพบตัวอย่างนี้ที่ใช้ได้กับ MRI 1.9 ถึง 2.1 สิ่งนี้ควรพิมพ์เป็นจำนวนมาก หากคุณไม่ได้ตั้งค่าตัวเลือก TCO เป็นจริงคุณควรได้รับข้อผิดพลาด "สแตกลึกเกินไป"

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
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.