Hiệu suất Ruby đã được cải thiện rất nhiều, phiên bản sau phiên bản… và đội ngũ phát triển Ruby đang nỗ lực mọi cách để làm Ruby nhanh hơn!
Một trong những nỗ lực này là dự án 3×3.
Mục tiêu?
Ruby 3.0 sẽ nhanh gấp 3 lần Ruby 2.0.
Một phần của dự án này là compiler MJIT mới, là chủ đề bài viết này.
Giải thích MJIT
MJIT là viết tắt của "Method Based Just-in-Time Compiler" (Compiler Just-In-Time dựa trên phương thức).
Điều đó nghĩa là gì?
Ruby compile code của bạn thành YARV instructions, các instruction này được chạy bởi Ruby Virtual Machine.
JIT thêm một layer khác vào đây.
Nó sẽ compile các instruction thường được dùng thành binary code.
Kết quả là binary tối ưu chạy code của bạn nhanh hơn.
Cách nó hoạt động
Hãy khám phá cách MJIT hoạt động để hiểu nó tốt hơn.
Bạn có thể bật JIT với Ruby 2.6 & tùy chọn --jit.
Như sau:
ruby --jit app.rb
Ruby 2.6 đi kèm một tập tùy chọn JIT cụ thể giúp chúng ta khám phá chính xác cách nó hoạt động. Bạn có thể thấy các tùy chọn này bằng cách chạy ruby --help.
Danh sách tùy chọn
- --jit-wait
- --jit-verbose
- --jit-save-temps
- --jit-max-cache
- --jit-min-calls
Tùy chọn verbose này trông như điểm khởi đầu tốt!
Chúng ta cũng sẽ dùng --jit-wait, điều này làm Ruby chờ cho đến khi compile JIT code xong rồi mới chạy.
Trong vận hành bình thường JIT compile code trong worker thread & không chờ nó xong.
Đây là lệnh bạn có thể chạy để test:
ruby --disable-gems --jit --jit-verbose=1 --jit-wait -e "4.times { 123 }"
Điều này in ra:
Successful MJIT finish
Đó không quá thú vị, phải không?
JIT không làm gì cả.
Tại sao?
Bởi vì mặc định, JIT chỉ xuất hiện khi phương thức được gọi 5 lần (jit-min-calls) hoặc nhiều hơn.
Nếu chúng ta chạy:
ruby --disable-gems --jit --jit-verbose=1 --jit-wait -e "5.times { 123 }"
Bây giờ chúng ta có điều thú vị:
JIT success (32.1ms): block in <main>@-e:1 -> /tmp/_ruby_mjit_p13921u0.c
Điều này nói gì?
JIT compile một block vì chúng ta gọi nó 5 lần, điều này cho bạn biết:
- Mất bao lâu để compile (
32.1ms) - Chính xác cái gì được compile (
block in <main>) - File được tạo (
/tmp/_ruby_mjit_p13921u0.c) làm source cho compile này
File này là C source code được compile thành object file (.o) & sau đó thành shared library file (.so).
Bạn có thể truy cập các file này nếu thêm tùy chọn --jit-save-temps.
Ví dụ:

Đây là hiểu biết hiện tại của tôi về cách JIT hoạt động:
- Đếm lời 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 - File C chứa instruction cho phương thức này được tạo (đây là YARV instruction, nhưng inlined)
- Compile xảy ra trong background (trừ khi
--jit-wait) dùng C compiler thông thường như GCC - Khi compile xong, file shared library kết quả được dùng khi phương thức này được gọi
Hãy xem nó hiệu quả như thế nào.
Test MJIT: Nó thực sự nhanh hơn không?
Mục tiêu của MJIT là làm Ruby nhanh hơn.
Nó làm tốt như thế nào bây giờ?
Hãy tìm hiểu!
Đầu tiên, microbenchmarks:
| Benchmark | Kết quả (So với Ruby 2.6 không JIT) |
|---|---|
| while | 8x nhanh hơn |
| while với string append | 10% nhanh hơn |
| while với nhân (Integer) | 4x nhanh hơn |
| while với nhân (Bignum) | 20% chậm hơn |
| string upcase | 10% nhanh hơn |
| string match | 2% chậm hơn |
| string match? | 10% nhanh hơn |
| array 10k random numbers | 20% nhanh hơn |
Có vẻ hiệu suất đi khắp nơi, nhưng có điều chúng ta có thể suy ra…
MJIT thực sự thích vòng lặp (loops)!
Nhưng nó ra sao với ứng dụng phức tạp hơn?
Hãy thử với một app Sinatra đơn giản:
require 'sinatra' get '/' do "apples, oranges & bananas" end
Có vẻ không nhiều, nhưng đoạn code nhỏ này chạy hơn 500 phương thức khác nhau. Đủ để cho JIT việc làm!
Để cụ thể, đây là Sinatra 2.0.4 với Thin 1.7.2.
Bạn có thể chạy benchmark với lệnh này (apache bench):
ab -c 20 -t 10 https://localhost:4567/
Kết quả:

Bạn có thể thấy từ đây Ruby 2.6 nhanh hơn 2.5, nhưng bật JIT làm Sinatra chậm 11%!
Tại sao?
Tôi không biết, có thể do overhead từ JIT, hoặc vì code không được tối ưu tốt.
Test của tôi với C profiler (callgrind) tiết lộ việc dùng code tối ưu JIT (file C compile được mà chúng ta khám phá trước) rất thấp cho Sinatra (dưới 2%), nhưng rất cao (24.22%) cho câu while được tăng tốc 8x.
Kết quả benchmark while với JIT:

Kết quả benchmark Sinatra với JIT:

Điều này có thể là một phần lý do, tôi không phải chuyên gia compiler nên không thể kết luận từ đây.
Tóm tắt
MJIT là "Just-in-Time Compiler" có sẵn trong Ruby 2.6, có thể bật với cờ --jit. MJIT đầy hứa hẹn & 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ẻ với bạn bè Ruby của bạn 🙂
Cảm ơn bạn đã đọc.