Hiệu suất của Ruby đã được cải thiện rất nhiều, hết phiên bản này đến phiên bản khác… và nhóm phát triển Ruby đang cố gắng hết sức để làm cho Ruby nhanh hơn nữa!
Một trong những nỗ lực này là dự án 3 × 3.
Mục tiêu?
Ruby 3.0 sẽ nhanh hơn 3 lần so với Ruby 2.0 .
Một phần của dự án này là trình biên dịch MJIT mới, là chủ đề của bài viết này.
Giải thích về MJIT
MJIT là viết tắt của “Trình biên dịch chỉ trong thời gian dựa trên phương pháp”.
Điều đó có nghĩa là gì?
Ruby biên dịch mã của bạn thành hướng dẫn YARV , các hướng dẫn này được chạy bởi Máy ảo Ruby.
JIT thêm một lớp khác vào lớp này.
Nó sẽ biên dịch các hướng dẫn thường được sử dụng thành mã nhị phân.
Kết quả là một tệp nhị phân được tối ưu hóa chạy mã của bạn nhanh hơn.
Cách hoạt động
Hãy cùng khám phá cách hoạt động của MJIT để hiểu rõ hơn về nó.
Bạn có thể kích hoạt JIT với Ruby 2.6 và --jit
tùy chọn.
Như thế này :
ruby --jit app.rb
Ruby 2.6 đi kèm với một tập hợp các tùy chọn dành riêng cho JIT sẽ giúp chúng ta khám phá chính xác cách thức hoạt động của nó. Bạn có thể thấy các tùy chọn này bằng cách chạy ruby --help
.
Đây là danh sách các tùy chọn
- –jit-wait
- –jit-verbose
- –jit-save-temps
- –jit-max-cache
- –jit-min-call
Chi tiết này tùy chọn có vẻ như là một điểm khởi đầu tốt!
Chúng tôi cũng sẽ sử dụng --jit-wait
, điều này khiến Ruby phải đợi cho đến khi quá trình biên dịch mã JIT hoàn tất trước khi chạy nó.
Trong quá trình hoạt động bình thường JIT biên dịch mã trong một chuỗi công nhân &nó không đợi nó kết thúc.
Đây là lệnh bạn có thể chạy để kiểm tra điều này:
ruby --disable-gems --jit --jit-verbose=1 --jit-wait -e "4.times { 123 }"
Bản in này :
Successful MJIT finish
Chà, điều đó không thú vị lắm phải không?
JIT không làm gì cả.
Tại sao?
Vì theo mặc định, JIT chỉ hoạt động khi một phương thức được gọi 5 lần (jit-min-calls
) trở lên.
Nếu chúng tôi chạy điều này:
ruby --disable-gems --jit --jit-verbose=1 --jit-wait -e "5.times { 123 }"
Bây giờ chúng tôi nhận được một cái gì đó thú vị :
JIT success (32.1ms): block in <main>@-e:1 -> /tmp/_ruby_mjit_p13921u0.c
Điều này nói lên điều gì?
JIT đã biên dịch một khối vì chúng tôi đã gọi nó 5 lần, điều này cho bạn biết:
- Mất bao lâu để biên dịch (
32.1ms
), - Chính xác những gì đã được biên dịch (
block in <main>
) - Tệp đã được tạo (
/tmp/_ruby_mjit_p13921u0.c
) làm nguồn cho phần biên dịch này
Tệp này là mã nguồn C được biên dịch thành tệp đối tượng (.o
) và sau đó vào tệp thư viện được chia sẻ (.so
).
Bạn có thể có quyền truy cập vào các tệp này nếu bạn thêm --jit-save-temps
tùy chọn.
Đây là một ví dụ :
Đây là hiểu biết hiện tại của tôi về cách hoạt động của JIT :
- Đếm số lần gọi phương thức
- Khi một phương thức được gọi 5 lần (mặc định cho
jit-min-calls
) kích hoạt JIT - Một tệp C chứa các hướng dẫn cho phương pháp này được tạo (đây là các hướng dẫn YARV, nhưng được nội dòng)
- Quá trình biên dịch diễn ra trong nền (trừ khi
--jit-wait
) bằng cách sử dụng trình biên dịch C thông thường như GCC - Khi quá trình biên dịch hoàn tất, tệp thư viện chia sẻ kết quả được sử dụng khi phương thức này được gọi
Hãy xem cách này hiệu quả như thế nào.
Kiểm tra MJIT:Nó có thực sự nhanh hơn không?
Mục tiêu của MJIT là làm cho Ruby nhanh hơn.
Hiện tại nó tốt đến mức nào?
Hãy cùng tìm hiểu!
Đầu tiên, microbenchmarks:
Điểm chuẩn | Kết quả (So với Ruby 2.6 không có JIT) |
---|---|
trong khi | Nhanh hơn 8 lần |
trong khi với phần nối chuỗi | nhanh hơn 10% |
trong khi với phép nhân (Số nguyên) | Nhanh hơn 4 lần |
trong khi với phép nhân (Bignum) | 20% chậm hơn |
viết hoa chuỗi | nhanh hơn 10% |
đối sánh chuỗi | chậm hơn 2% |
chuỗi khớp? | nhanh hơn 10% |
mảng có 10k số ngẫu nhiên | nhanh hơn 20% |
Có vẻ như hiệu suất là ở tất cả mọi nơi, nhưng có điều gì đó chúng ta có thể suy ra từ điều này…
MJIT thực sự thích vòng lặp!
Nhưng nó hoạt động như thế nào với một ứng dụng phức tạp hơn?
Hãy thử với một ứng dụng Sinatra đơn giản :
require 'sinatra' get '/' do "apples, oranges & bananas" end
Nó có thể không giống nhiều, nhưng đoạn mã nhỏ này chạy hơn 500 phương thức khác nhau. Đủ để cung cấp cho JIT một số công việc phải làm!
Cụ thể, đây là Sinatra 2.0.4 với Thin 1.7.2.
Bạn có thể chạy điểm chuẩn bằng lệnh này (băng ghế dự bị apache):
ab -c 20 -t 10 https://localhost:4567/
Đây là kết quả :
Bạn có thể biết từ những điều này rằng Ruby 2.6 nhanh hơn 2.5, nhưng việc bật JIT làm cho Sinatra chậm hơn 11% !
Tại sao?
Tôi không biết, có thể là do chi phí do JIT giới thiệu hoặc do mã không được tối ưu hóa tốt.
Thử nghiệm của tôi với trình biên dịch C (callgrind) cho thấy rằng việc sử dụng mã được tối ưu hóa JIT (các tệp C đã biên dịch mà chúng tôi đã phát hiện trước đó) là rất thấp đối với Sinatra (dưới 2% ), nhưng nó rất cao ( 24,22% ) cho câu lệnh while được tăng tốc độ lên gấp 8 lần.
Kết quả cho điểm chuẩn trong khi với JIT :
Kết quả cho điểm chuẩn Sinatra với JIT :
Đây có thể là một phần lý do, tôi không phải là chuyên gia biên dịch nên tôi không thể đưa ra bất kỳ kết luận nào từ điều này.
Tóm tắt
MJIT là một “Just-in-Time Compiler” có sẵn trong Ruby 2.6, nó có thể được kích hoạt bằng --jit
lá cờ. MJIT đầy hứa hẹn và có thể tăng tốc một số chương trình nhỏ, nhưng vẫn còn rất nhiều việc phải làm!
Nếu bạn thích bài viết này, đừng quên chia sẻ nó với những người bạn Ruby của bạn 🙂
Cảm ơn vì đã đọc.