LoginSignup
5
0

More than 1 year has passed since last update.

GHCのFFI&ForeignPtrに関するパフォーマンスの調査(C++編)

Last updated at Posted at 2022-12-21

この記事は、Haskell Advent Calendar 2022の22日目の記事です。

GHCのFFIとForeignPtrに関するパフォーマンスの調査をしました。
至らぬところがあるかと思いますが、よろしくおねがいします。

概要

Hasktorchというプロジェクトで行列の演算にpytorchのc++のライブラリ(libtorch)を使っています。
10x10の行列積の計算のベンチマークで、Hmatrixが1.03usに対して、Hasktorchでは3.21usかかりました。
2つのライブラリは構造がよく似ていて、オブジェクトの管理はどちらもForeignPtrを使っていますし、計算はCのライブラリのblasを使っています。違いはHmatrixがC言語のライブラリを扱っているのに対して、HasktorchはC++言語のライブラリを扱っているというところです。2usの違いがパフォーマンスに現れました。

CないしC++関数を使うにはFFIが必要です。Cの関数のパフォーマンスはよく調べられていて、
例えばこちらに行列計算のライブラリの一覧があり、FFIを使っているのがHmatrixで、Haskellだけで書かれているのがdense-linear-algebra(DLA)です。
CのFFI単体のベンチマークはこちらです。

これらのベンチマークからいえることは、実装の違いで数100ns程度の速度の差がでるということです。
数usの違いは出てこないように見えましたので、C++を使っているHasktorchが遅い原因を調べることにしました。

遅い原因は次の複合的なものでした。

  • C++のライブラリ(libtorch)が遅い
  • FFIのSafe Callを使っていた
  • C++の例外の補足のため
  • Inline展開されてない
  • ForeignPtrのc finalizerが遅い

C++のライブラリのベンチマーク

行列の計算以外のオーバーヘッドを調べるため、2x2の行列足し算についてベンチマークをしました。
https://discuss.pytorch.org/t/too-much-c-api-overhead/164381
C言語で書いたプログラムは0.309nsに対して、C++のライブラリ(libtorch)は894ns程度かかりました。

inline-c-cppのベンチマーク

inline-c-cppはC++の例外をHaskell側に返すために、例外の種類、例外のメッセージ、例外の型、例外の種類ごとのポインタといったデータが必要で、それぞれallocを使って領域を確保していました。
また、inline-c-cppはFFIのUnsafe Callに対応しておらず、結果一回の呼び出しに250ns程度かかっていました。

次のPRでinline-c-cppは改良済みです。
https://github.com/fpco/inline-c/pull/140

Inline展開

Inline展開が行われてないコードがあったので削除しました。
一つ一つは数十ns程度のオーバーヘッドですが、複数あって百ns程度のオーバーヘッドになっていました。

ForeignPtrのc finalizerのベンチマーク

GHCはC/C++のオブジェクトをGCで処理できるように削除されるときに呼ばれるc finalizerを設定できます。
c finalizerはweak pointerのリストで管理されています。c finalizer自身もリストになっています。
ezyangさんのblogにその説明があります。実際のベンチマークがなかったので、調べてみました。

単にメモリの確保を行うだけのベンチマークですが、
c finalizerを使うと146nsかかり、使わないと43.4nsと100ns程度の違いがあります。
https://github.com/junjihashimoto/ffi-benchmark

c finalizerが遅い原因は次のようなものでした。

  • weak pointerのリストで管理されている。(メモリのレイアウト的な問題)
  • weak pointerにあるc finalizerがリストで管理されている。(メモリのレイアウト的な問題)
    • つまりc finalizerはリストのリストで管理されている。(これで数十nsは失われます。)
  • c finalizerのセットに無駄が多い。
    • newForeignPtrを呼び出し、IORefで領域を確保、atomicなロックを使って、c finalizerをセット
    • 初期構築なのでlockを使わないで設定できるはず。
  • minor gcのたびにすべてのc finalizerのチェックが行われる。
    • copy gcの良さが台無しになり、gcの処理時間が増加。

Hmatrixはメモリの確保をGHCにまかせていて、c finalizerを使わないのでパフォーマンスがよいです。

まとめ

  • HmatrixとHasktorchを比較してc++のFFIが遅い原因を調べました。
  • 遅い原因は次の5つ(C++特有の問題は例外とc finalizerのところだけです。)
    • C++のライブラリ(libtorch)が遅い
    • C++の例外の補足のため
    • FFIのSafe Callを使っていた
    • Inline展開されてない
    • ForeignPtrのc finalizerが遅い
  • Hmatrixは上記の問題を回避しているからすごい。
5
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
0