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

Ruby ตีความและเรียกใช้โปรแกรมของคุณอย่างไร

ยิ่งคุณรู้เกี่ยวกับเครื่องมือของคุณมากเท่าไหร่ การตัดสินใจของคุณในฐานะนักพัฒนาก็จะยิ่งดีขึ้นเท่านั้น มักจะมีประโยชน์ โดยเฉพาะอย่างยิ่งเมื่อแก้ปัญหาด้านประสิทธิภาพ เพื่อทำความเข้าใจว่า 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 มันอาจจะไม่แก้ปัญหาของคุณ แต่อาจทำให้คุณเลิกคิดถึงมันได้ :)