2 điểm bởi GN⁺ 2025-02-14 | 1 bình luận | Chia sẻ qua WhatsApp

Có cách nào để cải thiện tốc độ FFI của CRuby không?

  • Khi cần gọi mã native từ Ruby, tốt nhất là viết càng nhiều mã Ruby càng tốt. Vì YJIT có thể tối ưu mã Ruby, nhưng không thể tối ưu mã C.
  • Khi gọi thư viện native, nên thực hiện phần lớn công việc trong Ruby và viết một native extension cung cấp API đơn giản để gọi các hàm native.
  • FFI không mang lại hiệu năng ngang với native extension. Ví dụ, nếu bọc hàm C strlen bằng FFI thì hiệu năng sẽ kém hơn so với C extension.

Kết quả benchmark

  • Gọi trực tiếp String#bytesize là nhanh nhất và có thể xem như mốc chuẩn.
  • Gọi strlen thông qua C extension là nhanh thứ hai, tiếp theo là gọi gián tiếp String#bytesize.
  • Triển khai FFI là chậm nhất. Điều này cho thấy việc gọi hàm native qua FFI tạo ra overhead đáng kể.

Có thể thay đổi thực tế này không?

  • Từ ý tưởng của Chris Seaton, bài viết đang khám phá khả năng tạo mã JIT để gọi các hàm bên ngoài.
  • Trong ví dụ wrapper FFI, khi gọi attach_function, có thể tạo ra mã máy cần thiết ngay tại thời điểm định nghĩa hàm wrapper.

Tận dụng RJIT

  • RJIT là trình biên dịch JIT được viết bằng Ruby và được cung cấp kèm theo Ruby.
  • RJIT được tách ra thành gem để các trình biên dịch JIT bên thứ ba có thể dễ dàng ánh xạ các cấu trúc dữ liệu của Ruby.
  • Luôn thực thi con trỏ hàm entry của JIT để JIT bên thứ ba có thể đăng ký vào mã máy.

Chứng minh khái niệm

  • Một bản chứng minh khái niệm nhỏ có tên "FJIT" có thể tạo mã máy trong lúc chạy để gọi các hàm bên ngoài.
  • Kết quả benchmark cho thấy mã máy do FJIT tạo ra nhanh hơn C extension và nhanh hơn hơn 2 lần so với lời gọi FFI.

Kết luận

  • Điều này cho thấy khả năng viết càng nhiều mã Ruby càng tốt trong khi vẫn giữ được tốc độ tương đương C extension (hoặc thậm chí nhanh hơn).
  • Ruby có thể có lợi thế là gọi được mã native mà không cần FFI.

Lưu ý

  • Hiện chỉ giới hạn trên nền tảng ARM64. Cần bổ sung backend x86_64.
  • Chưa xử lý mọi kiểu tham số và kiểu trả về. Hiện chỉ hỗ trợ một tham số và một giá trị trả về.
  • Cần chạy Ruby với các cờ --rjit --rjit-disable. Vấn đề này sẽ được giải quyết khi tính năng của Kokubun được áp dụng.
  • Hiện chỉ chạy được trên Ruby head.

1 bình luận

 
GN⁺ 2025-02-14
Ý kiến Hacker News
  • Đã phải xử lý rất nhiều FFI để gọi hàm giữa Java Constraint Solver (Timefold) và CPython

    • Vấn đề hiệu năng của FFI chủ yếu phát sinh từ việc dùng proxy để giao tiếp giữa ngôn ngữ chủ và ngôn ngữ ngoài
    • Các lời gọi FFI trực tiếp dùng JNI hoặc foreign interface mới thì nhanh, có tốc độ gần tương đương gọi trực tiếp phương thức Java
    • Tuy nhiên, bộ gom rác của CPython và Java không khớp nhau, nên cần các kỹ thuật đặc biệt để đồng bộ hóa
    • Khi dùng proxy như JPype hoặc GraalPy sẽ phát sinh overhead hiệu năng, đồng thời phải chuyển đổi tham số và giá trị trả về, và có thể phát sinh thêm các lời gọi FFI
    • Khi truyền một đối tượng CPython sang Java, Java sẽ giữ một proxy của đối tượng CPython đó
    • Nếu lại truyền proxy đó ngược về CPython thì sẽ tạo ra proxy của proxy
    • Kết quả là proxy của JPype chậm hơn 1402% so với việc gọi trực tiếp CPython qua FFI, còn proxy của GraalPy chậm hơn 453%
    • Cuối cùng, họ chuyển đổi bytecode CPython sang bytecode Java và tạo ra các cấu trúc dữ liệu Java tương ứng với các lớp CPython được sử dụng
    • Kết quả là đạt được mức cải thiện hiệu năng nhanh hơn 100 lần so với dùng proxy
    • Việc chuyển đổi hoặc đọc bytecode CPython rất thiếu ổn định và tài liệu còn nghèo nàn, đồng thời khó ánh xạ trực tiếp sang bytecode khác do nhiều đặc điểm kỳ lạ của VM
    • Có thể xem thêm chi tiết trong bài viết blog: liên kết
  • Nhờ blog Rails At Scale và byroot, đây là thời điểm rất tốt để quan tâm đến các cuộc thảo luận chuyên sâu về nội bộ và hiệu năng của Ruby

    • Nhờ những cải tiến gần đây của Ruby và Rails, hiện tại là một giai đoạn rất đáng vui với các Rubyist
  • Câu hỏi về việc liệu có thể JIT-compile mã để gọi hàm ngoài thay vì gọi thư viện bên thứ ba cho các lời gọi hàm ngoài hay không

    • Tôi tin đây là nguyên lý cơ bản của FFI trong LuaJIT: liên kết
    • Tôi nghĩ đó là lý do FFI của LuaJIT rất nhanh
  • Thông tin về một thư viện dùng JVMCI để sinh mã arm64/amd64 tại chỗ và gọi thư viện native mà không cần JNI: liên kết

  • Ý kiến rằng "hãy viết càng nhiều Ruby càng tốt, nhất là vì YJIT có thể tối ưu mã Ruby nhưng không thể tối ưu mã C"

    • Băn khoăn liệu Ruby chẳng phải là một ngôn ngữ khá chậm sao
    • Nếu đã đi vào native thì muốn xử lý càng nhiều việc càng tốt ở phía native
  • Đã dùng Ruby hơn 10 năm, và việc chứng kiến những tiến bộ gần đây thật sự rất thú vị

    • Rất đáng mong đợi
  • Thắc mắc vì sao cần JIT compile

    • Nghĩ rằng nếu có thể viết bằng C thì chẳng phải có thể compile ngay khi nạp hay sao
  • FFI - Foreign Function Interface, tức là cách để Ruby gọi C

  • Câu hỏi liệu đây chẳng phải chính là điều libffi làm hay sao

  • Có lẽ tôi hiểu vì sao họ không vào tenderlovemaking.com