Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Ruby

Hiểu về MJIT trong Ruby 2.6: Compiler Just-In-Time tăng hiệu suất như thế nào

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ụ:

Hiểu về MJIT trong Ruby 2.6: Compiler Just-In-Time tăng hiệu suất như thế nào

Đây là hiểu biết hiện tại của tôi về cách JIT hoạt động:

  1. Đếm lời gọi phương thức
  2. 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
  3. File C chứa instruction cho phương thức này được tạo (đây là YARV instruction, nhưng inlined)
  4. Compile xảy ra trong background (trừ khi --jit-wait) dùng C compiler thông thường như GCC
  5. 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ả:

Hiểu về MJIT trong Ruby 2.6: Compiler Just-In-Time tăng hiệu suất như thế nào

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:

Hiểu về MJIT trong Ruby 2.6: Compiler Just-In-Time tăng hiệu suất như thế nào

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

Hiểu về MJIT trong Ruby 2.6: Compiler Just-In-Time tăng hiệu suất như thế nào

Đ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.