ยินดีต้อนรับสู่ Ruby Magic article ใหม่! ครั้งนี้เราจะดูว่า Ruby interpret โค้ดของเราอย่างไร และเราสามารถใช้ knowledge นี้ได้อย่างไร โพสต์นี้จะช่วยเข้าใจว่า code ถูก interpret ยังไง และนำไปสู่ faster code ได้อย่างไร
ความแตกต่างเล็กน้อยระหว่าง Symbols
ใน Ruby Magic article ก่อนหน้าถึง Escaping characters in Ruby มีตัวอย่าง escaping line breaks
ในตัวอย่างด้านล่าง คุณเห็นว่า two strings รวมเป็น one String ข้าม multiple lines ด้วย plus + symbol หรือ backslash \
"foo" + "bar" => "foobar"
# versus
"foo" \ "bar" => "foobar"
ตัวอย่างทั้งสองอาจดูคล้ายกัน แต่ behave แตกต่างกันมาก เพื่อรู้ความแตกต่างระหว่างวิธีอ่านและ interpret คุณปกติต้องรู้ nitty-gritty เกี่ยวกับ Ruby interpreter หรือเราสามารถถาม Ruby เองได้ว่าแตกต่างอย่างไร
InstructionSequence
ใช้ class RubyVM::InstructionSequence เราสามารถถาม Ruby ว่ามัน interpret โค้ดที่เราให้อย่างไร Class นี้ให้ tool set ที่เราใช้ glimpse Ruby's internals ได้
สิ่งที่คืนในตัวอย่างด้านล่างคือ Ruby code ตามที่ YARV interpreter เข้าใจ
YARV Interpreter
YARV (Yet Another Ruby VM) เป็น Ruby interpreter ที่ introduced ใน Ruby version 1.9 แทนที่ original interpreter: MRI (Matz's Ruby Interpreter)
Languages ที่ใช้ interpreters execute code โดยตรงโดยไม่มี intermediate compilation step หมายความว่า Ruby ไม่ compile program ไป optimized machine language program ก่อน ซึ่ง compiled languages เช่น C, Rust และ Go ทำ
ใน Ruby โปรแกรมแรก translate ไป instruction set สำหรับ Ruby VM แล้ว execute ทันทีหลังจากนั้น Instructions เหล่านี้เป็น intermediate step ระหว่าง Ruby code ของคุณและ code ที่ execute ใน Ruby VM
Instructions ทำให้ Ruby VM เข้าใจ Ruby code ได้ง่ายขึ้นโดยไม่ต้องจัดการ syntax-specific interpretation สิ่งนั้น handled ขณะสร้าง instructions Instruction sequences เป็น optimized operations represent interpreted code
ระหว่าง normal execution ของ Ruby program เราไม่เห็น instructions เหล่านี้ แต่โดย viewing พวกเขา เราสามารถ review ได้ว่า Ruby interpret โค้ดของเราถูกต้องหรือไม่ ด้วย InstructionSequence สามารถดูได้ว่า YARV สร้าง instructions ประเภทไหนก่อน execute
ไม่จำเป็นต้องเข้าใจ YARV instructions ทั้งหมดที่ทำให้ Ruby interpreter ส่วนใหญ่ commands จะ speak for themselves
"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"]
...
Real output มี setup commands มากกว่านี้ซึ่งเราจะดูหลัง แต่ที่นี่เราเห็นความแตกต่างจริงระหว่าง "foo" + "bar" และ "foo" "bar"
ตัวแรกสร้าง two strings และ combine พวกเขา ตัวหลังสร้าง one string หมายความว่า ด้วย "foo" "bar" เราสร้างแค่ one string แทนที่จะเป็น three ด้วย "foo" + "bar"
1 2 3
↓ ↓ ↓
"foo" + "bar" # => "foobar"
แน่นอน นี่คือ basic example ที่สุด แต่แสดง use case ดีของรายละเอียดเล็กใน Ruby language ที่อาจมี impact มาก:
- More allocations: ทุก String object allocated แยกกัน
- More memory usage: ทุก allocated String object ใช้ memory
- Longer garbage collection: ทุก object แม้ short-lived ใช้เวลาถูก cleaned by garbage collector More allocations หมายถึง longer garbage collection times
Disassembling
Use case อีกอย่างคือ debug logic issue ตัวต่อไปนี้เป็น easy mistake ที่ทำได้ ซึ่งมี big consequences คุณ spot difference ได้ไหม?
1 + 2 * 3
# versus
(1 + 2) * 3
เราสามารถใช้ Ruby ช่วยหาความแตกต่างในตัวอย่าง slightly more complex นี้
ด้วยการ disassemble code example นี้ เราขอให้ Ruby พิมพ์ readable table ของ commands ที่มัน performing
1 + 2 * 3 # => 7
puts RubyVM::InstructionSequence.compile("1 + 2 * 3").disasm
# == disasm: @>==========
# 0000 trace 1 ( 1)
# 0002 putobject_OP_INT2FIX_O_1_C_
# 0003 putobject 2
# 0005 putobject 3
# 0007 opt_mult
# 0009 opt_plus
# 0011 leave
# versus
(1 + 2) * 3 # => 9
puts RubyVM::InstructionSequence.compile("(1 + 2) * 3").disasm
# == disasm: @>==========
# 0000 trace 1 ( 1)
# 0002 putobject_OP_INT2FIX_O_1_C_
# 0003 putobject 2
# 0005 opt_plus
# 0007 putobject 3
# 0009 opt_mult
# 0011 leave
ตัวอย่างข้างต้น involved กับจำนวน YARV instructions มากกว่า แต่จาก order ที่ things พิมพ์และ execute เราเห็นความแตกต่างที่ a pair of parentheses ทำได้
ด้วย parentheses รอบ 1 + 2 เราทำให้ addition perform ก่อน โดย move มันขึ้น order of operations ใน mathematics
สังเกตว่า you don't actually see the parentheses ใน disassembly output เอง แค่ effect ของพวกมันบน code ที่เหลือ
Disassembly
Disassembly output พิมพ์หลายอย่างที่อาจไม่เข้าใจทันที
ใน table format ที่พิมพ์ ทุกบรรทัดเริ่มด้วย operation number ตามด้วย operation และสุดท้าย argument ของ operation
Sample เล็กของ operations ที่เราเห็นมาจนตอนนี้:
trace- start a trace ดู docs on TracePoint เพื่อข้อมูลเพิ่มเติมputobject- push object onto stackputobject_OP_INT2FIX_O_1_C_- push Integer1onto stack Optimized operation (0และ1optimized)putstring- push string onto stackopt_plus- addition operation (internally optimized)opt_mult- multiply operation (internally optimized)leave- leave current code context
ตอนนี้เรารู้ว่า Ruby interpreter แปลง developer-friendly readable Ruby code ของเราเป็น YARV instructions อย่างไร เราสามารถใช้ optimize applications ได้
เป็นไปได้ที่จะ pass entire methods และแม้แต่ entire files ให้ RubyVM::InstructionSequence
puts RubyVM::InstructionSequence.disasm(method(:foo))
puts RubyVM::InstructionSequence.compile_file("/tmp/hello.rb").disasm
ค้นหาว่าทำไมบางโค้ดทำงาน และอีกบางไม่ทำ เรียนรู้ว่าทำไม symbols บางอย่างทำให้ code behave แตกต่าง devil is in the details และดีที่จะรู้ว่า Ruby code ของคุณ behave ใน app อย่างไร และ optimize ได้ไหม
Optimizations
นอกจาก view code บน interpreter level และ optimize สำหรับมัน คุณสามารถใช้ InstructionSequence optimize code อีก
ด้วย InstructionSequence สามารถ optimize บาง instructions ด้วย Ruby's built-in performance optimizations รายการ optimizations ทั้งหมด available ใน documentation ของ RubyVM::InstructionSequence.compile_option = method
หนึ่งใน optimizations คือ Tail Call Optimization
เมธอด RubyVM::InstructionSequence.compile รับ options enable optimization นี้แบบนี้:
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
คุณแม้แต่เปิด optimization นี้สำหรับโค้ดทั้งหมดด้วย RubyVM::InstructionSequence.compile_option = แค่แน่ใจว่า load นี้ก่อนโค้ดอื่นๆ
RubyVM::InstructionSequence.compile_option = { tailcall_optimization: true, trace_instruction: false }
ข้อมูลเพิ่มเติมเกี่ยว Tail Call Optimization ใน Ruby ดูได้จาก articles: Tail Call Optimization in Ruby และ Tail Call Optimization in Ruby: Background
Conclusion
เรียนรู้เพิ่มเติมเกี่ยวกับ Ruby interpret โค้ดของคุณด้วย RubyVM::InstructionSequence และดูว่าโค้ดทำอะไรจริงๆ เพื่อทำให้มัน performant มากขึ้น
Introduction นี้ถึง InstructionSequence อาจเป็น fun way เรียนรู้เพิ่มเติมเกี่ยวกับ Ruby works under the hood ใครรู้? คุณอาจสนใจ work บน Ruby's code เอง
นี่คือสรุป short introduction to code compilation ใน Ruby เรายินดีรู้ความคิดเห็นว่าชอบบทความนี้หรือไม่ มีคำถามอะไร และอยากอ่านเรื่องไหนต่อไป บอกเราที่ @AppSignal ได้เลย