Computer >> Máy Tính >  >> Lập trình >> Ruby

Cách Ruby thông dịch và chạy chương trình của bạn

Bạn càng biết nhiều về các công cụ của mình, bạn sẽ đưa ra quyết định tốt hơn với tư cách là nhà phát triển. Nó thường hữu ích - đặc biệt là khi gỡ lỗi các vấn đề về hiệu suất - để hiểu những gì Ruby thực sự đang làm khi nó chạy chương trình của bạn.

Trong bài đăng này, chúng ta sẽ theo dõi hành trình của một chương trình đơn giản vì nó được ghép từ, phân tích cú pháp và biên dịch thành mã bytecode. Chúng tôi sẽ sử dụng các công cụ mà Ruby cung cấp cho chúng tôi để theo dõi trình thông dịch từng bước trên đường đi.

Đừng lo lắng - ngay cả khi bạn không phải là chuyên gia, bài đăng này sẽ khá dễ theo dõi. Đó là một chuyến tham quan có hướng dẫn hơn là một hướng dẫn kỹ thuật.

Làm quen với chương trình mẫu của chúng tôi

Ví dụ, tôi sẽ sử dụng một câu lệnh if / else duy nhất. Để tiết kiệm dung lượng, tôi sẽ viết điều này bằng cách sử dụng toán tử bậc ba. Nhưng đừng để bị lừa, đó chỉ là if / else.

x > 100 ? 'foo' : 'bar'

Như bạn sẽ thấy, ngay cả một chương trình đơn giản như thế này cũng được dịch thành khá nhiều dữ liệu khi nó được xử lý.

Lưu ý:Tất cả các ví dụ trong bài đăng này được viết bằng Ruby (MRI) 2.2. Nếu bạn đang sử dụng các triển khai khác của Ruby, chúng có thể sẽ không hoạt động.

Mã hóa

Trước khi trình thông dịch Ruby có thể chạy chương trình của bạn, nó phải chuyển đổi nó từ một ngôn ngữ lập trình dạng tự do thành dữ liệu có cấu trúc hơn.

Bước đầu tiên có thể là chia nhỏ chương trình thành nhiều phần. Các phần này được gọi là mã thông báo.

# This is a string
"x > 1"

# These are tokens
["x", ">", "1"]

Thư viện chuẩn của Ruby cung cấp một mô-đun có tên là Ripper cho phép chúng tôi xử lý mã Ruby theo cách giống như trình thông dịch Ruby.

Trong ví dụ dưới đây, chúng tôi đang sử dụng phương thức mã hóa trên mã Ruby của chúng tôi. Như bạn có thể thấy, nó trả về một mảng các mã thông báo.

require 'ripper'
Ripper.tokenize("x > 1 ? 'foo' : 'bar'")
# => ["x", " ", ">", " ", "1", " ", "?", " ", "'", "foo", "'", " ", ":", " ", "'", "bar", "'"]

Tokenizer khá ngu ngốc. Bạn có thể cung cấp cho nó Ruby hoàn toàn không hợp lệ và nó vẫn sẽ mã hóa nó.

# bad code
Ripper.tokenize("1var @= \/foobar`")
# => ["1", "var"]

Lexing

Lexing là một bước vượt xa quá trình mã hóa. Chuỗi vẫn được chia thành các mã thông báo, nhưng dữ liệu bổ sung được thêm vào các mã thông báo.

Trong ví dụ dưới đây, chúng tôi đang sử dụng Ripper để Lex chương trình nhỏ của chúng tôi. như bạn có thể thấy, nó hiện đang gắn thẻ mỗi mã thông báo là một số nhận dạng :on_ident , một toán tử :on_op , một số nguyên :on_int , v.v.

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, "'"]]

Vẫn không có cú pháp thực sự kiểm tra nào diễn ra vào thời điểm này. Lexer sẽ vui vẻ xử lý mã không hợp lệ.

Phân tích cú pháp

Bây giờ Ruby đã chia nhỏ mã thành các phần dễ quản lý hơn, đã đến lúc bắt đầu phân tích cú pháp.

Trong giai đoạn phân tích cú pháp, Ruby biến đổi văn bản thành một thứ gọi là cây cú pháp trừu tượng, hoặc AST. Cây cú pháp trừu tượng là một đại diện của chương trình của bạn trong bộ nhớ.

Bạn có thể nói rằng các ngôn ngữ lập trình nói chung chỉ là những cách mô tả cây cú pháp trừu tượng thân thiện hơn với người dùng.

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]]]]]]]

Có thể không dễ dàng để đọc đầu ra này, nhưng nếu bạn nhìn vào nó đủ lâu, bạn có thể thấy nó ánh xạ như thế nào với chương trình gốc.

# 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]]]]]]]

Tại thời điểm này, trình thông dịch Ruby biết chính xác những gì bạn muốn nó làm. Nó có thể chạy chương trình của bạn ngay bây giờ. Và trước Ruby 1.9, nó sẽ có. Nhưng bây giờ, còn một bước nữa.

Biên dịch sang bytecode

Thay vì duyệt trực tiếp cây cú pháp trừu tượng, ngày nay Ruby biên dịch cây cú pháp trừu tượng thành mã byte cấp thấp hơn.

Mã byte này sau đó được chạy bởi máy ảo Ruby.

Chúng ta có thể xem qua hoạt động bên trong của máy ảo thông qua RubyVM::InstructionSequence lớp. Trong ví dụ dưới đây, chúng tôi biên dịch chương trình mẫu của mình và sau đó tháo rời nó để con người có thể đọc được.

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

Ái chà! Điều này đột nhiên trông giống như một ngôn ngữ hợp ngữ hơn là Ruby. Hãy bước qua nó và xem liệu chúng ta có thể hiểu được nó hay không.

# 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

Sau đó, máy ảo ruby ​​(YARV) sẽ thực hiện các bước hướng dẫn này và thực thi chúng. Vậy là xong!

Kết luận

Điều này kết thúc chuyến tham quan rất đơn giản, hoạt hình của chúng tôi về trình thông dịch Ruby. Với các công cụ mà tôi đã chỉ cho bạn ở đây, bạn có thể phỏng đoán rất nhiều về cách Ruby diễn giải các chương trình của bạn. Ý tôi là, nó không bê tông hơn AST. Và lần tới khi bạn gặp phải vấn đề hiệu suất kỳ lạ nào đó, hãy thử xem mã bytecode. Nó có thể sẽ không giải quyết được vấn đề của bạn, nhưng nó có thể khiến bạn mất trí nhớ. :)