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

Xem cách Ruby diễn giải mã của bạn

Chào mừng bạn đến với một bài viết mới của Ruby Magic! Lần này chúng ta sẽ xem xét cách Ruby diễn giải mã của chúng ta và cách chúng ta có thể sử dụng kiến ​​thức này để có lợi cho mình. Bài đăng này sẽ giúp bạn hiểu cách mã được diễn giải và cách điều này có thể giúp dẫn đến mã nhanh hơn.

Một sự khác biệt nhỏ giữa các ký hiệu

Trong một bài viết trước của Ruby Magic về Thoát các ký tự trong Ruby, có một ví dụ về thoát ngắt dòng.

Trong ví dụ bên dưới, bạn thấy cách hai chuỗi được kết hợp thành một Chuỗi trên nhiều dòng, với dấu cộng + biểu tượng hoặc với dấu gạch chéo ngược \ .

"foo" +
  "bar"
=> "foobar"
 
# versus
 
"foo" \
  "bar"
=> "foobar"

Hai ví dụ này có thể trông giống nhau, nhưng chúng hoạt động khá khác nhau. Để biết sự khác biệt giữa cách chúng được đọc và thông dịch, thông thường bạn cần biết thực tế về trình thông dịch Ruby. Hoặc, chúng ta có thể hỏi Ruby sự khác biệt là gì.

InstructionSequence

Sử dụng RubyVM::InstructionSequence chúng ta có thể hỏi Ruby làm thế nào nó diễn giải một số mã mà chúng ta cung cấp cho nó. Lớp này cung cấp cho chúng ta một bộ công cụ mà chúng ta có thể sử dụng để tìm hiểu sơ lược về nội bộ của Ruby.

Những gì được trả về trong ví dụ dưới đây là mã Ruby vì nó được trình thông dịch YARV hiểu.

Trình thông dịch YARV

YARV (Yet Another Ruby VM) là trình thông dịch Ruby được giới thiệu trong phiên bản Ruby 1.9, thay thế trình thông dịch gốc:MRI (Trình thông dịch Ruby của Matz).

Các ngôn ngữ sử dụng trình thông dịch trực tiếp thực thi mã mà không cần qua bước biên dịch trung gian. Điều này có nghĩa là Ruby không phải lần đầu tiên biên dịch một chương trình sang một chương trình ngôn ngữ máy được tối ưu hóa, mà đã biên dịch các ngôn ngữ như C, Rust và Go.

Trong Ruby, một chương trình đầu tiên được dịch sang một tập lệnh cho máy ảo Ruby, và sau đó được thực thi ngay sau đó. Các hướng dẫn này là bước trung gian giữa mã Ruby của bạn và mã đang được thực thi trong máy ảo Ruby.

Những hướng dẫn này giúp Ruby VM hiểu mã Ruby dễ dàng hơn mà không cần phải giải thích cú pháp cụ thể. Điều đó được xử lý trong khi tạo các hướng dẫn này. Trình tự lệnh là các hoạt động được tối ưu hóa đại diện cho mã được thông dịch.

Trong quá trình thực thi bình thường của một chương trình Ruby, chúng tôi không thấy những hướng dẫn này, nhưng bằng cách xem chúng, chúng tôi có thể xem lại liệu Ruby có diễn giải mã của chúng tôi một cách chính xác hay không. Với InstructionSequence có thể xem YARV tạo ra những hướng dẫn nào trước khi thực thi chúng.

Không cần thiết phải hiểu tất cả các hướng dẫn YARV tạo nên trình thông dịch Ruby. Hầu hết các lệnh sẽ tự nói.

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

Đầu ra thực chứa nhiều lệnh thiết lập hơn một chút mà chúng ta sẽ xem xét sau, nhưng ở đây chúng ta có thể thấy sự khác biệt thực sự giữa "foo" + "bar""foo" "bar" .

Đầu tiên tạo ra hai chuỗi và kết hợp chúng. Sau đó tạo ra một chuỗi. Điều này có nghĩa là với "foo" "bar" chúng tôi chỉ tạo một chuỗi thay vì ba chuỗi với "foo" + "bar" .

  1       2           3
  ↓       ↓           ↓
"foo" + "bar" # => "foobar"

Tất nhiên, đây chỉ là ví dụ cơ bản nhất mà chúng ta có thể sử dụng, nhưng nó cho thấy một trường hợp sử dụng tốt về cách một chi tiết nhỏ trong ngôn ngữ Ruby có thể có rất nhiều tác động:

  • Phân bổ nhiều hơn:mọi đối tượng Chuỗi được phân bổ riêng biệt.
  • Sử dụng nhiều bộ nhớ hơn:mọi đối tượng Chuỗi được cấp phát đều chiếm bộ nhớ.
  • Thu gom rác lâu hơn:mọi đồ vật, ngay cả khi tồn tại trong thời gian ngắn, cần có thời gian để được người thu gom rác làm sạch. Phân bổ nhiều hơn đồng nghĩa với thời gian thu gom rác lâu hơn.

Tháo rời

Một trường hợp sử dụng khác là gỡ lỗi một vấn đề logic. Sau đây là sai lầm dễ mắc phải, có thể gây hậu quả lớn. Bạn có thể nhận ra sự khác biệt không?

1 + 2 * 3
# versus
(1 + 2) * 3

Chúng tôi có thể sử dụng Ruby để giúp chúng tôi tìm ra sự khác biệt trong ví dụ phức tạp hơn một chút này.

Bằng cách tháo rời ví dụ mã này, chúng ta có thể giúp Ruby in một bảng dễ đọc hơn về các lệnh mà nó đang thực hiện.

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

Ví dụ trên có liên quan nhiều hơn một chút đến số lượng lệnh YARV, nhưng chỉ từ thứ tự mà mọi thứ được in và thực thi, chúng ta thấy sự khác biệt mà một cặp dấu ngoặc đơn có thể tạo ra.

Với dấu ngoặc đơn xung quanh 1 + 2 chúng tôi đảm bảo phép cộng được thực hiện trước bằng cách di chuyển nó lên theo thứ tự các phép toán trong toán học.

Lưu ý rằng bạn không thực sự nhìn thấy dấu ngoặc đơn trong chính đầu ra gỡ bỏ, chỉ ảnh hưởng của chúng đến phần còn lại của mã.

Tháo gỡ

Đầu ra Disassembly in ra nhiều thứ có thể không hiểu ngay lập tức.

Trong định dạng bảng được in, mỗi dòng bắt đầu bằng một số hoạt động. Sau đó, nó đề cập đến hoạt động và cuối cùng là đối số của hoạt động.

Một số mẫu hoạt động nhỏ mà chúng tôi đã thấy cho đến nay:

  • trace - bắt đầu một dấu vết. Xem tài liệu trên TracePoint để biết thêm thông tin.
  • putobject - đẩy một đối tượng lên ngăn xếp.
  • putobject_OP_INT2FIX_O_1_C_ - đẩy Số nguyên 1 trên ngăn xếp. Hoạt động tối ưu. (01 được tối ưu hóa.)
  • putstring - đẩy một chuỗi lên ngăn xếp.
  • opt_plus - hoạt động bổ sung (được tối ưu hóa nội bộ).
  • opt_mult - hoạt động nhân (được tối ưu hóa nội bộ).
  • leave - để lại ngữ cảnh mã hiện tại.

Bây giờ chúng ta đã biết cách trình thông dịch Ruby chuyển đổi mã Ruby thân thiện và dễ đọc cho nhà phát triển của chúng ta thành các lệnh YARV, chúng ta có thể sử dụng điều này để tối ưu hóa các ứng dụng của mình.

Có thể chuyển toàn bộ phương thức và thậm chí toàn bộ tệp sang RubyVM::InstructionSequence .

puts RubyVM::InstructionSequence.disasm(method(:foo))
puts RubyVM::InstructionSequence.compile_file("/tmp/hello.rb").disasm

Tìm hiểu lý do tại sao một số đoạn mã hoạt động và tại sao một đoạn mã khác không hoạt động. Tìm hiểu lý do tại sao một số ký hiệu nhất định làm cho mã hoạt động khác với những ký hiệu khác. Ma quỷ nằm ở chi tiết và thật tốt khi biết mã Ruby của bạn đang hoạt động như thế nào trong ứng dụng của bạn và nếu bạn có thể tối ưu hóa nó theo bất kỳ cách nào.

Tối ưu hóa

Ngoài việc có thể xem mã của bạn ở cấp trình thông dịch và tối ưu hóa cho nó, bạn có thể sử dụng InstructionSequence để tối ưu hóa mã của bạn hơn nữa.

Với InstructionSequence , có thể tối ưu hóa một số hướng dẫn nhất định với tính năng tối ưu hóa hiệu suất tích hợp của Ruby. Danh sách đầy đủ các tối ưu hóa có sẵn trong RubyVM::InstructionSequence.compile_option = tài liệu phương pháp.

Một trong những cách tối ưu hóa này là Tối ưu hóa cuộc gọi đuôi .

RubyVM::InstructionSequence.compile phương pháp chấp nhận các tùy chọn để kích hoạt tính năng tối ưu hóa này, chẳng hạn như:

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

Bạn thậm chí có thể bật tính năng tối ưu hóa này cho tất cả mã của mình bằng RubyVM::InstructionSequence.compile_option = . Chỉ cần đảm bảo tải mã này trước bất kỳ mã nào khác của bạn.

RubyVM::InstructionSequence.compile_option = {
  tailcall_optimization: true,
  trace_instruction: false
}

Để biết thêm thông tin về cách hoạt động của Tối ưu hóa cuộc gọi đuôi trong Ruby, hãy xem các bài viết sau:Tối ưu hóa cuộc gọi đuôi trong Ruby và Tối ưu hóa cuộc gọi đuôi trong Ruby:Cơ sở.

Kết luận

Tìm hiểu thêm về cách Ruby diễn giải mã của bạn với RubyVM::InstructionSequence và xem mã của bạn thực sự đang làm gì để bạn có thể làm cho mã hoạt động hiệu quả hơn.

Phần giới thiệu về InstructionSequence này cũng có thể là một cách thú vị để tìm hiểu thêm về cách hoạt động của Ruby. Ai biết? Bạn thậm chí có thể quan tâm đến việc làm việc trên chính một số mã của Ruby.

Điều đó kết thúc phần giới thiệu ngắn của chúng tôi về biên dịch mã trong Ruby. Chúng tôi muốn biết bạn thích bài viết này như thế nào, nếu bạn có bất kỳ câu hỏi nào về nó và những gì bạn muốn đọc tiếp theo, vì vậy hãy nhớ cho chúng tôi biết tại @AppSignal.