CRubyのFFI速度を向上させる方法はあるのか?
- Rubyでネイティブコードを呼び出す必要がある場合は、できるだけ多くのRubyコードを書くのが望ましい。YJITはRubyコードを最適化できるが、Cコードは最適化できないため。
- ネイティブライブラリを呼び出す際は、Ruby側で処理の大半を行い、ネイティブ関数呼び出しのためのシンプルなAPIを提供するネイティブ拡張を書くのがよい。
- FFIはネイティブ拡張と同等の性能を提供しない。たとえば、
strlen C関数をFFIでラップした場合、C拡張と比べて性能が劣る。
ベンチマーク結果
String#bytesize を直接呼び出すのが最も速く、これは基準点と考えられる。
- C拡張経由の
strlen 呼び出しが2番目に速く、間接的に String#bytesize を呼び出す方法がその次。
- FFI実装が最も遅い。これは、FFI経由でネイティブ関数を呼び出す際にかなりのオーバーヘッドが発生することを示している。
現状を変えられるか?
- Chris Seatonのアイデアにより、外部関数を呼び出すためのJITコードを生成できる可能性を探っている。
- FFIラッパーの例では、
attach_function 呼び出し時に、ラッパー関数の定義時点で必要な機械語コードを生成できる。
RJITの活用
- RJITはRubyで書かれたJITコンパイラで、Rubyに同梱されている。
- RJITをgemとして切り出し、3rdパーティ製JITコンパイラがRubyのデータ構造を簡単にマッピングできるようにする。
- JITエントリ関数ポインタを常に実行し、3rdパーティ製JITが機械語コードに登録できるようにする。
概念実証
- 「FJIT」という小さな概念実証により、ランタイム時に機械語コードを生成して外部関数を呼び出せる。
- ベンチマーク結果では、FJITが生成した機械語コードはC拡張より高速で、FFI呼び出しより2倍以上速い。
結論
- C拡張と同等の速度(あるいはそれ以上の速度)を維持しながら、可能な限り多くのRubyコードを書ける可能性を示している。
- RubyがFFIなしでネイティブコードを呼び出せるという利点を持てる可能性がある。
注意事項
- 現時点ではARM64プラットフォームのみに限定される。x86_64バックエンドを追加する必要がある。
- すべての引数型と戻り値型を扱えるわけではない。単一の引数と戻り値のみ扱える。
- Rubyを
--rjit --rjit-disable フラグ付きで実行する必要がある。Kokubunの機能が適用されれば解決する見込み。
- 現在はRuby headでのみ実行可能。
1件のコメント
Hacker News のコメント
Java Constraint Solver(Timefold)と CPython 間の関数呼び出しのために、FFI をかなり扱う必要があった
Rails At Scale と byroot のブログのおかげで、今は Ruby の内部実装と性能について踏み込んだ議論に関心を持つには良い時期だ
外部関数呼び出しのために 3rd party ライブラリを呼ぶ代わりに、コードを JIT コンパイルできないかという質問
JVMCI を使って arm64/amd64 コードをその場で生成し、JNI なしでネイティブライブラリを呼び出すライブラリに関する情報: リンク
「できるだけ多くの Ruby を書くべきだ。特に YJIT は Ruby コードは最適化できるが、C コードは最適化できないから」という意見
10 年以上 Ruby を使ってきたが、最近の進展を見るのはとても興味深い
なぜ JIT コンパイルが必要なのかという疑問
FFI - Foreign Function Interface、つまり Ruby から C を呼び出す方法
これこそ libffi がやっていることではないのかという質問
tenderlovemaking.com に行かなかった理由が分かる気がする