2 ポイント 投稿者 GN⁺ 2025-02-14 | 1件のコメント | WhatsAppで共有

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件のコメント

 
GN⁺ 2025-02-14
Hacker News のコメント
  • Java Constraint Solver(Timefold)と CPython 間の関数呼び出しのために、FFI をかなり扱う必要があった

    • FFI の性能問題は主に、ホスト言語と外部言語の間の通信のためにプロキシを使うことから生じる
    • JNI や新しい foreign interface を使った直接的な FFI 呼び出しは高速で、Java メソッドを直接呼ぶのとほぼ同じ速度である
    • しかし、CPython と Java のガベージコレクタは相性が良くなく、同期のために特別な技術が必要になる
    • JPype や GraalPy のようなプロキシを使うと性能オーバーヘッドが発生し、引数や戻り値を変換しなければならず、追加の FFI 呼び出しが発生することもある
    • CPython オブジェクトを Java に渡すと、Java はその CPython オブジェクトに対するプロキシを持つ
    • そのプロキシを再び CPython に渡すと、プロキシのプロキシが生成される
    • その結果、JPype のプロキシは CPython を直接 FFI で呼び出すより 1402% 遅く、GraalPy のプロキシは 453% 遅い
    • 最終的には CPython バイトコードを Java バイトコードに変換し、使用されている CPython クラスに対応する Java データ構造を生成した
    • その結果、プロキシを使うより 100 倍高速な性能向上を得た
    • CPython バイトコードを変換または読み取ることは非常に不安定で、ドキュメントも乏しく、VM のさまざまな癖のために他のバイトコードへ直接マッピングするのは難しい
    • 詳細はブログ記事を参照できる: リンク
  • Rails At Scale と byroot のブログのおかげで、今は Ruby の内部実装と性能について踏み込んだ議論に関心を持つには良い時期だ

    • 最近の Ruby と Rails の改善のおかげで、Rubyist として良い時期である
  • 外部関数呼び出しのために 3rd party ライブラリを呼ぶ代わりに、コードを JIT コンパイルできないかという質問

    • LuaJIT FFI の基本原理だと確信している: リンク
    • LuaJIT の FFI が非常に高速な理由だと思う
  • JVMCI を使って arm64/amd64 コードをその場で生成し、JNI なしでネイティブライブラリを呼び出すライブラリに関する情報: リンク

  • 「できるだけ多くの Ruby を書くべきだ。特に YJIT は Ruby コードは最適化できるが、C コードは最適化できないから」という意見

    • Ruby はかなり遅い言語ではないのかという疑問
    • ネイティブに入るなら、できるだけ多くの処理をネイティブ側で行いたい
  • 10 年以上 Ruby を使ってきたが、最近の進展を見るのはとても興味深い

    • 楽しみだ
  • なぜ JIT コンパイルが必要なのかという疑問

    • C で書けるなら、ロード時にコンパイルできるのではないかという考え
  • FFI - Foreign Function Interface、つまり Ruby から C を呼び出す方法

  • これこそ libffi がやっていることではないのかという質問

  • tenderlovemaking.com に行かなかった理由が分かる気がする