Computer >> คอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

ดูวิธีที่ Ruby ตีความโค้ดของคุณ

ยินดีต้อนรับสู่บทความ Ruby Magic ใหม่! คราวนี้เราจะมาดูกันว่า Ruby ตีความโค้ดของเราอย่างไร และเราจะนำความรู้นี้ไปใช้ให้เกิดประโยชน์ได้อย่างไร โพสต์นี้จะช่วยให้คุณเข้าใจว่าโค้ดถูกตีความอย่างไร และสิ่งนี้จะช่วยนำไปสู่โค้ดที่เร็วขึ้นได้อย่างไร

ความแตกต่างเล็กน้อยระหว่างสัญลักษณ์

ในบทความก่อนหน้าของ Ruby Magic เกี่ยวกับการหลบหนีของตัวละครใน Ruby มีตัวอย่างเกี่ยวกับการหลบหนีการขึ้นบรรทัดใหม่

ในตัวอย่างด้านล่าง คุณจะเห็นวิธีการรวมสตริงสองสตริงเป็นสตริงเดียวในหลายบรรทัด โดยใช้เครื่องหมายบวก + สัญลักษณ์หรือแบ็กสแลช \ .

"foo" +
  "bar"
=> "foobar"
 
# versus
 
"foo" \
  "bar"
=> "foobar"

ตัวอย่างทั้งสองนี้อาจดูคล้ายกัน แต่มีพฤติกรรมแตกต่างกันมาก หากต้องการทราบความแตกต่างระหว่างวิธีการอ่านและตีความ โดยปกติแล้ว คุณจะต้องรู้ข้อมูลสำคัญเกี่ยวกับล่าม Ruby หรือเราจะถาม Ruby ว่าต่างกันอย่างไร

ลำดับคำสั่ง

การใช้ RubyVM::InstructionSequence เราสามารถถาม Ruby ว่ามันตีความโค้ดที่เราให้มาได้อย่างไร คลาสนี้จะมีชุดเครื่องมือให้เราใช้เพื่อดูอวัยวะภายในของ Ruby

สิ่งที่ส่งคืนในตัวอย่างด้านล่างคือรหัส Ruby ตามที่ล่าม YARV เข้าใจ

ล่าม YARV

YARV (Yet Another Ruby VM) เป็นล่าม Ruby ที่เปิดตัวใน Ruby เวอร์ชัน 1.9 แทนที่ตัวแปลดั้งเดิม:MRI (Ruby Interpreter ของ Matz)

ภาษาที่ใช้ล่ามรันโค้ดโดยตรงโดยไม่มีขั้นตอนการคอมไพล์ระดับกลาง ซึ่งหมายความว่า Ruby ไม่ได้คอมไพล์โปรแกรมเป็นโปรแกรมภาษาเครื่องที่ปรับให้เหมาะสมก่อน ซึ่งคอมไพล์ภาษาเช่น C, Rust และ Go ทำ

ใน Ruby โปรแกรมจะได้รับการแปลเป็นชุดคำสั่งสำหรับ Ruby VM ก่อน จากนั้นจึงดำเนินการทันที คำแนะนำเหล่านี้เป็นขั้นตอนกลางระหว่างรหัส Ruby ของคุณกับรหัสที่กำลังดำเนินการใน Ruby VM

คำแนะนำเหล่านี้ช่วยให้ Ruby VM เข้าใจโค้ด Ruby ได้ง่ายขึ้นโดยไม่ต้องจัดการกับการตีความเฉพาะทางไวยากรณ์ ที่จัดการในขณะที่สร้างคำแนะนำเหล่านี้ ลำดับของคำสั่งเป็นการดำเนินการที่ปรับให้เหมาะสมซึ่งแสดงถึงโค้ดที่แปลแล้ว

ระหว่างการทำงานปกติของโปรแกรม Ruby เราไม่เห็นคำแนะนำเหล่านี้ แต่เมื่อดูแล้ว เราจะสามารถตรวจสอบได้ว่า Ruby ตีความโค้ดของเราอย่างถูกต้องหรือไม่ ด้วย InstructionSequence เป็นไปได้ที่จะเห็นว่าคำสั่งประเภทใดที่ YARV สร้างก่อนที่จะดำเนินการ

ไม่จำเป็นต้องเข้าใจคำสั่ง YARV ทั้งหมดที่ประกอบขึ้นเป็นล่าม Ruby คำสั่งส่วนใหญ่จะพูดเพื่อตัวเอง

"foo" +
  "bar"
RubyVM::InstructionSequence.compile('"foo" + "bar"').to_a
# ... [:putstring, "foo"], [:putstring, "bar"] ...
 
# versus
 
"foo" \
  "bar"
RubyVM::InstructionSequence.compile('"foo" "bar"').to_a
# ... [:putstring, "foobar"] ...

ผลลัพธ์จริงมีคำสั่งการตั้งค่าอีกเล็กน้อยที่เราจะดูในภายหลัง แต่ที่นี่เราจะเห็นความแตกต่างที่แท้จริงระหว่าง "foo" + "bar" และ "foo" "bar" .

อดีตสร้างสองสตริงและรวมเข้าด้วยกัน หลังสร้างหนึ่งสตริง ซึ่งหมายความว่าด้วย "foo" "bar" เราสร้างสตริงเดียวเท่านั้น แทนที่จะสร้างสามสตริงด้วย "foo" + "bar" .

  1       2           3
  ↓       ↓           ↓
"foo" + "bar" # => "foobar"

แน่นอนว่านี่เป็นเพียงตัวอย่างพื้นฐานที่สุดที่เราสามารถใช้ได้ แต่แสดงให้เห็นกรณีการใช้งานที่ดีว่ารายละเอียดเล็กๆ น้อยๆ ในภาษา Ruby อาจส่งผลกระทบได้มาก:

  • การจัดสรรเพิ่มเติม:ทุกอ็อบเจ็กต์ String ได้รับการจัดสรรแยกกัน
  • การใช้หน่วยความจำมากขึ้น:ทุกอ็อบเจ็กต์สตริงที่จัดสรรจะใช้หน่วยความจำ
  • เก็บขยะได้นานขึ้น:ทุกวัตถุแม้จะมีอายุสั้นก็ต้องใช้เวลาในการทำความสะอาดโดยตัวเก็บขยะ การจัดสรรมากขึ้นหมายถึงเวลาในการเก็บขยะนานขึ้น

การถอดประกอบ

กรณีการใช้งานอื่นกำลังดีบักปัญหาตรรกะ ต่อไปนี้เป็นข้อผิดพลาดง่าย ๆ ที่จะทำ ซึ่งอาจมีผลกระทบใหญ่ คุณเห็นความแตกต่างไหม

1 + 2 * 3
# versus
(1 + 2) * 3

เราสามารถใช้ Ruby เพื่อช่วยเราค้นหาความแตกต่างในตัวอย่างที่ซับซ้อนกว่านี้เล็กน้อย

การแยกส่วนตัวอย่างโค้ดนี้จะทำให้ Ruby พิมพ์ตารางคำสั่งที่กำลังทำงานได้ง่ายขึ้น

1 + 2 * 3
# => 7
puts RubyVM::InstructionSequence.compile("1 + 2 * 3").disasm
# == disasm: <RubyVM::InstructionSequence:<compiled>@<compiled>>==========
# 0000 trace            1                                               (   1)
# 0002 putobject_OP_INT2FIX_O_1_C_
# 0003 putobject        2
# 0005 putobject        3
# 0007 opt_mult         <callinfo!mid:*, argc:1, ARGS_SIMPLE>
# 0009 opt_plus         <callinfo!mid:+, argc:1, ARGS_SIMPLE>
# 0011 leave
 
# versus
 
(1 + 2) * 3
# => 9
puts RubyVM::InstructionSequence.compile("(1 + 2) * 3").disasm
# == disasm: <RubyVM::InstructionSequence:<compiled>@<compiled>>==========
# 0000 trace            1                                               (   1)
# 0002 putobject_OP_INT2FIX_O_1_C_
# 0003 putobject        2
# 0005 opt_plus         <callinfo!mid:+, argc:1, ARGS_SIMPLE>
# 0007 putobject        3
# 0009 opt_mult         <callinfo!mid:*, argc:1, ARGS_SIMPLE>
# 0011 leave

ตัวอย่างข้างต้นเกี่ยวข้องกับจำนวนคำสั่ง YARV เล็กน้อย แต่จากลำดับการพิมพ์และดำเนินการ เราจะเห็นความแตกต่างที่วงเล็บคู่สามารถทำได้

โดยมีวงเล็บล้อมรอบ 1 + 2 เราตรวจสอบให้แน่ใจว่าได้ดำเนินการเพิ่มก่อน โดยเพิ่มลำดับการดำเนินการในวิชาคณิตศาสตร์

โปรดทราบว่าคุณไม่เห็นวงเล็บในผลลัพธ์การถอดแยกชิ้นส่วนจริง ๆ แต่จะมีผลกับโค้ดที่เหลือเท่านั้น

การถอดประกอบ

เอาต์พุต Disassembly พิมพ์สิ่งต่างๆ มากมายที่อาจไม่เข้าใจในทันที

ในรูปแบบตารางที่พิมพ์ ทุกบรรทัดจะเริ่มต้นด้วยหมายเลขการดำเนินการ หลังจากนั้นก็กล่าวถึงการดำเนินการและสุดท้ายอาร์กิวเมนต์ของการดำเนินการ

ตัวอย่างการดำเนินการเล็กๆ น้อยๆ ที่เราเคยเห็น:

  • trace - เริ่มการติดตาม ดูเอกสารใน TracePoint สำหรับข้อมูลเพิ่มเติม
  • putobject - ดันวัตถุบนสแต็ก
  • putobject_OP_INT2FIX_O_1_C_ - กดเลขจำนวนเต็ม 1 บนสแต็ก เพิ่มประสิทธิภาพการทำงาน (0 และ 1 ถูกปรับให้เหมาะสม)
  • putstring - ดันสตริงบนสแต็ก
  • opt_plus - การดำเนินการเพิ่มเติม (เพิ่มประสิทธิภาพภายใน)
  • opt_mult - การดำเนินการทวีคูณ (ปรับให้เหมาะสมภายใน)
  • leave - ปล่อยให้บริบทโค้ดปัจจุบัน

ตอนนี้เรารู้แล้วว่าล่าม Ruby แปลงรหัส Ruby ที่เป็นมิตรกับนักพัฒนาและอ่านได้เป็นคำสั่ง YARV ได้อย่างไร เราจึงสามารถใช้สิ่งนี้เพื่อเพิ่มประสิทธิภาพแอปพลิเคชันของเราได้

เป็นไปได้ที่จะส่งต่อวิธีการทั้งหมดและแม้แต่ไฟล์ทั้งหมดไปยัง RubyVM::InstructionSequence .

puts RubyVM::InstructionSequence.disasm(method(:foo))
puts RubyVM::InstructionSequence.compile_file("/tmp/hello.rb").disasm

ค้นหาว่าเหตุใดโค้ดบางส่วนจึงใช้งานได้และทำไมโค้ดอื่นถึงใช้ไม่ได้ เรียนรู้ว่าเหตุใดสัญลักษณ์บางตัวจึงทำให้โค้ดมีพฤติกรรมแตกต่างจากตัวอื่น มารอยู่ในรายละเอียด และเป็นการดีที่จะรู้ว่ารหัส Ruby ของคุณทำงานอย่างไรในแอปของคุณ และหากคุณสามารถเพิ่มประสิทธิภาพได้ไม่ว่าในทางใด

การเพิ่มประสิทธิภาพ

นอกเหนือจากการดูโค้ดของคุณในระดับล่ามและปรับให้เหมาะสมแล้ว คุณสามารถใช้ InstructionSequence เพื่อเพิ่มประสิทธิภาพโค้ดของคุณให้ดียิ่งขึ้น

ด้วย InstructionSequence เป็นไปได้ที่จะเพิ่มประสิทธิภาพคำสั่งบางอย่างด้วยการเพิ่มประสิทธิภาพในตัวของ Ruby รายการการปรับให้เหมาะสมทั้งหมดมีอยู่ใน RubyVM::InstructionSequence.compile_option = เอกสารวิธีการ

หนึ่งในการเพิ่มประสิทธิภาพเหล่านี้คือ Tail Call Optimization .

RubyVM::InstructionSequence.compile method ยอมรับตัวเลือกเพื่อเปิดใช้งานการปรับให้เหมาะสมเช่นนี้:

some_code = <<-EOS
def fact(n, acc=1)
  return acc if n <= 1
  fact(n-1, n*acc)
end
EOS
puts RubyVM::InstructionSequence.compile(some_code, nil, nil, nil, tailcall_optimization: true, trace_instruction: false).disasm
RubyVM::InstructionSequence.compile(some_code, nil, nil, nil, tailcall_optimization: true, trace_instruction: false).eval

คุณยังสามารถเปิดการปรับแต่งนี้สำหรับโค้ดทั้งหมดของคุณด้วย RubyVM::InstructionSequence.compile_option = . อย่าลืมโหลดสิ่งนี้ก่อนโค้ดอื่นๆ ของคุณ

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

สำหรับข้อมูลเพิ่มเติมเกี่ยวกับวิธีการทำงานของ Tail Call Optimization ใน Ruby โปรดดูบทความเหล่านี้:Tail Call Optimization ใน Ruby และ Tail Call Optimization ใน Ruby:พื้นหลัง

บทสรุป

เรียนรู้เพิ่มเติมเกี่ยวกับวิธีที่ Ruby ตีความโค้ดของคุณด้วย RubyVM::InstructionSequence และดูว่าโค้ดของคุณทำอะไรได้บ้าง เพื่อให้คุณได้มีประสิทธิภาพมากขึ้น

การแนะนำ InstructionSequence นี้อาจเป็นวิธีที่สนุกในการเรียนรู้เพิ่มเติมเกี่ยวกับวิธีการทำงานของ Ruby ภายใต้ประทุน ใครจะรู้? คุณอาจสนใจที่จะทำงานกับโค้ดของ Ruby ด้วยซ้ำ

ซึ่งเป็นการสรุปการแนะนำสั้น ๆ ของเราเกี่ยวกับการรวบรวมโค้ดใน Ruby เราชอบที่จะรู้ว่าคุณชอบบทความนี้อย่างไร หากคุณมีคำถามเกี่ยวกับบทความนี้ และต้องการอ่านเกี่ยวกับอะไรต่อไป โปรดแจ้งให้เราทราบที่ @AppSignal