- YJIT và ZJIT là các kiến trúc trình biên dịch JIT trong Ruby 3.x, chuyển mã Ruby sang mã máy để tăng tốc độ thực thi
- YJIT đếm số lần gọi của từng hàm hoặc block và khi đạt một ngưỡng nhất định sẽ chuyển đoạn mã đó sang mã máy
- Mã đã chuyển đổi được lưu trong YJIT block, mỗi block sẽ chuyển nhiều lệnh YARV tương ứng thành các lệnh mã máy ARM64
- Sử dụng Branch Stub để quan sát kiểu dữ liệu thực tế tại runtime và chọn lọc sinh ra các lệnh mã máy phù hợp
- Cấu trúc này là cơ chế cốt lõi để đồng thời đạt được cải thiện hiệu năng thực thi của Ruby và hiệu quả xử lý kiểu động
Chương 4: Biên dịch Ruby thành mã máy
Diễn dịch vs. Biên dịch mã Ruby
- Bản gốc không có nội dung chi tiết
Đếm số lần gọi hàm và block
- YJIT theo dõi số lần gọi hàm và block của chương trình để xác định đoạn mã hotspot
- Bên cạnh chuỗi lệnh YARV của mỗi hàm hoặc block, nó lưu các giá trị jit_entry và jit_entry_calls
jit_entry ban đầu là null, về sau sẽ lưu con trỏ tới mã máy do YJIT sinh ra
jit_entry_calls tăng thêm 1 mỗi khi được gọi
- Khi số lần gọi đạt ngưỡng, YJIT sẽ biên dịch đoạn mã đó sang mã máy
- Ngưỡng mặc định của Ruby 3.5 là 30 lần với chương trình nhỏ và 120 lần với ứng dụng lớn
- Có thể thay đổi khi chạy bằng tùy chọn
--yjit-call-threshold
- Với cách này, YJIT chỉ chuyển những đoạn mã được thực thi thường xuyên sang mã máy để đảm bảo đường thực thi hiệu quả
YJIT Blocks
- YJIT lưu các lệnh mã máy đã tạo trong YJIT block
- YJIT block khác với Ruby block, và tương ứng với một phần đoạn lệnh YARV
- Mỗi hàm hoặc block Ruby được cấu thành từ nhiều YJIT block
- Trong chương trình ví dụ, khi block được thực thi lần thứ 30 thì YJIT bắt đầu biên dịch
- Nó chuyển lệnh YARV đầu tiên
getlocal_WC_1 sang mã máy và tạo một YJIT block mới
- Sau đó tiếp tục biên dịch lệnh
getlocal_WC_0 và đưa vào cùng block đó
- Theo Figure 4-8, YJIT sinh ra các lệnh ARM64 để nạp giá trị vào các thanh ghi x1, x9 của bộ xử lý M1
getlocal_WC_1 lưu biến cục bộ của stack frame trước đó, còn getlocal_WC_0 lưu biến của stack hiện tại lên stack
- Các lệnh mã máy được sinh ra thực hiện cùng một hành vi
YJIT Branch Stubs
- Khi YJIT biên dịch lệnh
opt_plus, phát sinh vấn đề không biết kiểu của toán hạng
- Tùy theo kiểu như số nguyên, chuỗi, số thực dấu chấm động..., các lệnh mã máy cần dùng sẽ khác nhau
- Ví dụ: phép cộng số nguyên dùng lệnh
adds, còn phép cộng số thực dấu chấm động cần lệnh khác
- Để giải quyết việc này, YJIT không phân tích trước mà quan sát tại runtime
- Trong lúc chương trình chạy, nó kiểm tra kiểu của các giá trị thực sự được truyền vào rồi sinh mã máy phù hợp
- Cơ chế này sử dụng Branch Stub
- Khi một nhánh (branch) mới chưa có block nào được nối vào, nó tạm thời được nối tới stub
- Sau khi xác nhận được kiểu thực tế, stub đó sẽ được thay bằng block thích hợp
ZJIT (chỉ được nhắc đến)
- Mục lục có phần liên quan đến ZJIT, nhưng phần thân bài không có giải thích cụ thể
Tóm tắt
- YJIT là trình biên dịch JIT trong Ruby 3.5 nhằm nâng cao hiệu quả thực thi của ngôn ngữ kiểu động
- Các điểm cốt lõi là kích hoạt biên dịch dựa trên số lần gọi, cấu trúc YJIT block và xác định kiểu tại runtime thông qua Branch Stub
- Nó chuyển đổi thành các lệnh mã máy thực tế trên kiến trúc ARM64 để cải thiện tốc độ thực thi của mã Ruby
- ZJIT được nhắc đến như JIT thế hệ tiếp theo, nhưng bài viết không có nội dung chi tiết
1 bình luận
Ý kiến Hacker News
Trước đây từng có thời MacRuby biên dịch ra mã máy native trên macOS bằng LLVM và tích hợp với framework Objective‑C
Đây là một ý tưởng khá hay, nhưng cuối cùng có vẻ Apple đã chuyển hướng sang Swift
Nếu có phiên bản mới, mình nhất định sẽ mua và đọc cuốn Ruby Under a Microscope. Mình vẫn rất thích Ruby, nhưng thực tế lại không có nhiều cơ hội dùng đến
Giờ người khác đang tiếp tục duy trì, nhưng có vẻ hiện tại họ tập trung hơn vào DragonRuby (một implementation Ruby thiên về game)
Tham khảo thêm bài viết trên wiki
Tuy nhiên, các API cũ có thể không còn được hỗ trợ nữa
VB6 có tốc độ phát triển thực sự rất nhanh, và còn có thể làm cả Direct3D lẫn ASP Classic
Sự thanh lịch và tiện lợi khi phát triển của Ruby khiến mình nhớ lại thời đó
Nếu Ruby có công cụ GUI ở tầm VB6, có lẽ mức độ phổ biến của nó đã rất khác
Thật sự rất vui khi thấy Pat vẫn tiếp tục theo đuổi dự án này
Cuốn Ruby Under a Microscope đầu tiên và các bài blog của anh ấy đã mang lại cho mình rất nhiều cảm hứng
Mình cũng từng gặp anh ấy trực tiếp ở hội nghị Euruko, và anh ấy thực sự là một người tuyệt vời
Lần đầu đọc Ruby Under a Microscope mình thấy cực kỳ thú vị
Nhờ đó mà trước đây mình còn áp dụng được vào cả giải bài CTF
Dạo này mình không còn theo sát phần triển khai nội bộ của Ruby nữa, nhưng nếu có bản mới thì chắc chắn sẽ mua
Bài viết này khiến mình muốn đọc lại phiên bản sách mới
Nhân nói về chuyện biên dịch Ruby, mình tò mò không biết có ai từng thử Sorbet compiler do các lập trình viên Stripe tạo ra chưa
Bài công bố Sorbet Compiler
Biên dịch AOT thực sự rất khó với Ruby
Điều thú vị trong cách tiếp cận của Sorbet là nó có thể tạo ra đường chạy nhanh dựa trên kiểm tra kiểu của Ruby
Mình cũng đang làm một trình biên dịch Ruby như một dự án cá nhân, và đang tham khảo hokstad.com/compiler cùng
writing-a-compiler-in-ruby
Hiện tại mình đang tập trung vào việc vượt qua RubySpec, và sau này cũng định thử tối ưu hóa dựa trên kiểu
Dù không liên quan trực tiếp đến biên dịch Ruby, cuốn Enterprise Integration with Ruby đã cho mình rất nhiều gợi mở về cách sử dụng Ruby ngoài lĩnh vực web
Từ khi biết đến MRuby, mình bị cuốn vào niềm vui biến các dự án và script của mình thành file thực thi độc lập
Mình rất vui vì Ruby Under a Microscope vẫn đang tiếp tục được cập nhật
Với những ai muốn hiểu cách Ruby vận hành bên trong, mình nghĩ đây là một cuốn sách bắt buộc phải đọc
Mình từng thắc mắc khi một block của YJIT được chạy nhiều lần thì nó theo dõi việc biên dịch theo từng kiểu đầu vào như thế nào
Mình muốn hiểu Ruby xử lý các kiểu khác nhau như int hay float ra sao
Nó dùng cách tiếp cận "chờ rồi xem" bằng cách trì hoãn biên dịch cho đến khi có kiểu thực tế được cung cấp
Nó quản lý riêng các phiên bản block cho từng kiểu, rồi gọi phiên bản phù hợp theo ngữ cảnh
Thuật toán này được gọi là Basic Block Versioning
Maxime Chevalier‑Boisvert của Shopify giải thích rất rõ trong video thuyết trình tại RubyConf 2021
Có vẻ engine JIT mới là ZJIT dùng một cách tiếp cận khác
Việc tăng tốc ngôn ngữ kiểu động bằng JIT thường phải đánh đổi bằng mức sử dụng bộ nhớ tăng lên
Nếu không phải công ty lớn như Shopify thì đây có thể là vấn đề lớn hơn
Các instance cloud ngày nay thường có khoảng 4GiB RAM mỗi core, nên vài trăm MB mã JIT là hoàn toàn có thể gánh được
Cách YJIT chỉ đếm số lần gọi hàm để tìm điểm nóng có vẻ hơi đơn giản
Mình tự hỏi liệu nó có cơ chế phát hiện các phép tính nặng bên trong vòng lặp như JIT của JavaScript không
Có vẻ cấu trúc block của Ruby cũng có thể giúp ích cho kiểu tối ưu hóa này
Nên JIT có thể coi block như một hàm riêng và tối ưu hóa vòng lặp một cách tự nhiên
Phần này sẽ được bàn sâu hơn trong chương tiếp theo