ยินดีต้อนรับสู่บทความ 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