ยิ่งคุณรู้เกี่ยวกับเครื่องมือของคุณมากเท่าไหร่ การตัดสินใจของคุณในฐานะนักพัฒนาก็จะยิ่งดีขึ้นเท่านั้น มักจะมีประโยชน์ โดยเฉพาะอย่างยิ่งเมื่อแก้ปัญหาด้านประสิทธิภาพ เพื่อทำความเข้าใจว่า Ruby กำลังทำอะไรอยู่เมื่อรันโปรแกรมของคุณ
ในโพสต์นี้ เราจะติดตามการเดินทางของโปรแกรมอย่างง่าย เนื่องจากเป็น lexed แยกวิเคราะห์ และคอมไพล์เป็น bytecode เราจะใช้เครื่องมือที่ Ruby มอบให้เพื่อสอดแนมล่ามในทุกขั้นตอน
ไม่ต้องกังวล แม้ว่าคุณจะไม่ใช่ผู้เชี่ยวชาญ โพสต์นี้ควรติดตามได้ง่าย เป็นไกด์ทัวร์มากกว่าคู่มือทางเทคนิค
พบกับโปรแกรมตัวอย่างของเรา
ตัวอย่างเช่น ฉันจะใช้คำสั่ง if/else เดียว เพื่อประหยัดพื้นที่ ฉันจะเขียนสิ่งนี้โดยใช้ตัวดำเนินการ ternary แต่อย่าหลงกล มันเป็นแค่ if/else
x > 100 ? 'foo' : 'bar'
อย่างที่คุณเห็น แม้แต่โปรแกรมธรรมดาๆ แบบนี้ก็ยังได้รับการแปลเป็นข้อมูลจำนวนมากในขณะที่ประมวลผล
หมายเหตุ:ตัวอย่างทั้งหมดในโพสต์นี้เขียนด้วย Ruby (MRI) 2.2 หากคุณกำลังใช้งาน Ruby อื่นๆ อยู่ มันอาจจะใช้งานไม่ได้
การทำโทเค็น
ก่อนที่ล่าม Ruby จะเรียกใช้โปรแกรมของคุณได้ จะต้องแปลงจากภาษาการเขียนโปรแกรมที่ค่อนข้างอิสระเป็นข้อมูลที่มีโครงสร้างมากขึ้น
ขั้นตอนแรกอาจเป็นการแบ่งโปรแกรมออกเป็นชิ้นๆ ชิ้นส่วนเหล่านี้เรียกว่าโทเค็น
# This is a string
"x > 1"
# These are tokens
["x", ">", "1"]
ไลบรารีมาตรฐาน Ruby มีโมดูลที่เรียกว่า Ripper ซึ่งช่วยให้เราประมวลผลโค้ด Ruby ได้ในลักษณะเดียวกับตัวแปล Ruby
ในตัวอย่างด้านล่าง เรากำลังใช้วิธี tokenize ในโค้ด Ruby ของเรา อย่างที่คุณเห็น มันจะส่งคืนอาร์เรย์ของโทเค็น
require 'ripper'
Ripper.tokenize("x > 1 ? 'foo' : 'bar'")
# => ["x", " ", ">", " ", "1", " ", "?", " ", "'", "foo", "'", " ", ":", " ", "'", "bar", "'"]
tokenizer ค่อนข้างโง่ คุณสามารถป้อน Ruby ที่ไม่ถูกต้องโดยสมบูรณ์ได้ และมันจะยังคงสร้างโทเค็นให้กับมัน
# bad code
Ripper.tokenize("1var @= \/foobar`")
# => ["1", "var"]
เล็กซิ่ง
Lexing เป็นขั้นตอนหนึ่งนอกเหนือจากการสร้างโทเค็น สตริงยังคงแบ่งออกเป็นโทเค็น แต่ข้อมูลเพิ่มเติมจะถูกเพิ่มไปยังโทเค็น
ในตัวอย่างด้านล่าง เรากำลังใช้ Ripper เพื่อ Lex โปรแกรมขนาดเล็กของเรา อย่างที่คุณเห็น ตอนนี้มันกำลังแท็กโทเค็นแต่ละตัวเป็นตัวระบุ :on_ident
, โอเปอเรเตอร์ :on_op
, จำนวนเต็ม :on_int
, ฯลฯ
require 'ripper'
require 'pp'
pp Ripper.lex("x > 100 ? 'foo' : 'bar'")
# [[[1, 0], :on_ident, "x"],
# [[1, 1], :on_sp, " "],
# [[1, 2], :on_op, ">"],
# [[1, 3], :on_sp, " "],
# [[1, 4], :on_int, "100"],
# [[1, 5], :on_sp, " "],
# [[1, 6], :on_op, "?"],
# [[1, 7], :on_sp, " "],
# [[1, 8], :on_tstring_beg, "'"],
# [[1, 9], :on_tstring_content, "foo"],
# [[1, 12], :on_tstring_end, "'"],
# [[1, 13], :on_sp, " "],
# [[1, 14], :on_op, ":"],
# [[1, 15], :on_sp, " "],
# [[1, 16], :on_tstring_beg, "'"],
# [[1, 17], :on_tstring_content, "bar"],
# [[1, 20], :on_tstring_end, "'"]]
ยังไม่มีการตรวจสอบไวยากรณ์ที่แท้จริงเกิดขึ้น ณ จุดนี้ lexer จะประมวลผลโค้ดที่ไม่ถูกต้องอย่างมีความสุข
การแยกวิเคราะห์
เมื่อ Ruby ได้แบ่งโค้ดเป็นส่วนๆ ที่จัดการได้มากขึ้น ก็ถึงเวลาเริ่มการแยกวิเคราะห์
ในระหว่างขั้นตอนการแยกวิเคราะห์ Ruby จะแปลงข้อความเป็นสิ่งที่เรียกว่าแผนผังไวยากรณ์นามธรรมหรือ AST ต้นไม้ไวยากรณ์นามธรรมเป็นตัวแทนของโปรแกรมของคุณในหน่วยความจำ
คุณอาจกล่าวได้ว่าภาษาโปรแกรมโดยทั่วไปเป็นเพียงวิธีที่ใช้งานง่ายกว่าในการอธิบายแผนผังไวยากรณ์นามธรรม
require 'ripper'
require 'pp'
pp Ripper.sexp("x > 100 ? 'foo' : 'bar'")
# [:program,
# [[:ifop,
# [:binary, [:vcall, [:@ident, "x", [1, 0]]], :>, [:@int, "100", [1, 4]]],
# [:string_literal, [:string_content, [:@tstring_content, "foo", [1, 11]]]],
# [:string_literal, [:string_content, [:@tstring_content, "foobar", [1, 19]]]]]]]
การอ่านผลลัพธ์นี้อาจไม่ใช่เรื่องง่าย แต่ถ้าคุณจ้องเป็นเวลานานพอ คุณจะเห็นว่ามันจับคู่กับโปรแกรมต้นฉบับอย่างไร
# Define a progam
[:program,
# Do an "if" operation
[[:ifop,
# Check the conditional (x > 100)
[:binary, [:vcall, [:@ident, "x", [1, 0]]], :>, [:@int, "100", [1, 4]]],
# If true, return "foo"
[:string_literal, [:string_content, [:@tstring_content, "foo", [1, 11]]]],
# If false, return "bar"
[:string_literal, [:string_content, [:@tstring_content, "foobar", [1, 19]]]]]]]
ณ จุดนี้ ล่าม Ruby รู้ดีว่าคุณต้องการทำอะไร มันสามารถเรียกใช้โปรแกรมของคุณได้ในขณะนี้ และก่อน Ruby 1.9 ก็จะมี แต่ตอนนี้ มีอีกหนึ่งขั้นตอน
กำลังรวบรวมเป็น bytecode
แทนที่จะสำรวจโครงสร้างไวยากรณ์นามธรรมโดยตรง ทุกวันนี้ Ruby รวบรวมแผนผังไวยากรณ์นามธรรมเป็นโค้ดไบต์ระดับล่าง
รหัสไบต์นี้เรียกใช้โดยเครื่องเสมือน Ruby
เราสามารถดูการทำงานภายในของเครื่องเสมือนผ่าน RubyVM::InstructionSequence
ระดับ. ในตัวอย่างด้านล่าง เรารวบรวมโปรแกรมตัวอย่างของเราแล้วแยกส่วนเพื่อให้มนุษย์สามารถอ่านได้
puts RubyVM::InstructionSequence.compile("x > 100 ? 'foo' : 'bar'").disassemble
# == disasm: <RubyVM::InstructionSequence:<compiled>@<compiled>>==========
# 0000 trace 1 ( 1)
# 0002 putself
# 0003 opt_send_without_block <callinfo!mid:x, argc:0, FCALL|VCALL|ARGS_SIMPLE>
# 0005 putobject 100
# 0007 opt_gt <callinfo!mid:>, argc:1, ARGS_SIMPLE>
# 0009 branchunless 15
# 0011 putstring "foo"
# 0013 leave
# 0014 pop
# 0015 putstring "bar"
# 0017 leave
โว้ว! นี่ดูเหมือนภาษาแอสเซมบลีมากกว่า Ruby มาก ลองก้าวผ่านมันไปและดูว่าเราจะเข้าใจมันไหม
# Call the method `x` on self and save the result on the stack
0002 putself
0003 opt_send_without_block <callinfo!mid:x, argc:0, FCALL|VCALL|ARGS_SIMPLE>
# Put the number 100 on the stack
0005 putobject 100
# Do the comparison (x > 100)
0007 opt_gt <callinfo!mid:>, argc:1, ARGS_SIMPLE>
# If the comparison was false, go to line 15
0009 branchunless 15
# If the comparison was true, return "foo"
0011 putstring "foo"
0013 leave
0014 pop
# Here's line 15. We jumped here if comparison was false. Return "bar"
0015 putstring "bar"
0017 leave
เครื่องเสมือนทับทิม (YARV) จะทำตามขั้นตอนเหล่านี้และดำเนินการตามคำแนะนำเหล่านี้ แค่นั้นแหละ!
บทสรุป
จบการทัวร์ล่าม Ruby ในรูปแบบการ์ตูนที่ดูเรียบง่าย ด้วยเครื่องมือที่ฉันได้แสดงให้คุณเห็นที่นี่ เป็นไปได้ที่จะใช้การคาดเดามากมายว่า Ruby ตีความโปรแกรมของคุณอย่างไร ฉันหมายความว่ามันไม่ได้เป็นรูปธรรมมากกว่า AST และครั้งต่อไปที่คุณสะดุดกับปัญหาด้านประสิทธิภาพแปลกๆ ให้ลองดูที่ bytecode มันอาจจะไม่แก้ปัญหาของคุณ แต่อาจทำให้คุณเลิกคิดถึงมันได้ :)