@kenmaroです。
普段は主に秘密計算、準同型暗号などの記事について投稿しています。
秘密計算に関連するまとめの記事に関しては以下をご覧ください。
概要
なにかライブラリを実装したりするとき、
Pythonでサクッと作ってしまおうか、C++でがっつり作ってしまおうか、
などと考えることは誰でもあると思います。
この際、
- Pythonが得意とするところ
- C++が得意とするところ
それらをどう構成させ、
どのような機能を持った実装が必要なのかを今一度考えてから実装するべき、
というのがこの記事の要点の一つ目。
必要ならブリッジするような実装も考える場合、
このような際、いくつか選択肢が存在しますが、
今回はバインディングのみを考えるのであれば
pybind11が最強の選択肢であった、
ということをお伝えしたいのがこの記事の二点目。
になります。
今回は、C++ To Python をする時の候補
Cythonとpybind11において、
実際にどのようなところがpybind11が優れていると思うのかなどについてまとめつつ、
PythonやC++のいいとこ取りをした実装について少しでも考えるきっかけになったらと思います。
##C++、Python、ポーティングに関して前もって知っておくべきこと
もしC++が〜Pythonが〜と悩んでいる人がいるとするなら、
以下の観点でしっかりとどちらの言語で実装するべきか、
また、いいとこ取りをするような実装も可能であることも念頭に置きつつ、
一度考えてみる必要があります。
###Python, C++ 両方いいことを知る
- C++でのコードはポーティングせずにC++からそのまま使うことが最速である。
当たり前ですが、Pythonはとても使い勝手が良く、プロトタイプを作る際や、データ分析、AIモデルの構築などを行う際は最高の言語だと考えていますが、速度面ではやはり物足りないところがあります。
numpyなどを駆使して高速な実装をしていくことは可能ですが、numpy, pandas, tensorflowやkerasなど、Pythonを使うことで得られるライブラリの恩恵を受けることが必要ない場合、C++などの言語で実装を考えた方が無難でしょう。
もし上記のライブラリをガンガン使ってデータ分析や処理などをする場合は、C++で実装など考えず、Pythonが最強でしょう。
あとは好みもあると思いますし、最終的には自分の判断ですが、
明らかにPythonでの実装が特性を活かせる、C++での実装の方が特性を活かせる
というシチュエーションでは、好みよりも特性を優先するべきでしょう。
そんなこと何度も聞いたよ、と思われると思いますが、
実際作りたいライブラリやサービスにどの程度の速度要求や汎用性が必要かということについて今一度考え、
どの部分はPythonで、どの部分はC++(やRust)で、
もしくは全てPythonでいいのか、全てC++でいいのか
などを一度考えることも大切だ、ということについてまずお伝えしました。
プロトタイプ作りであればPythonで爆速で実装し、あとでC++で高速化したいところを洗い出す、
みたいな感じでもいいと思います。
明らかに後々速度がネックになると考えるのであれば、
もともとC++やRustなどの言語で下位レイヤから上位レイヤまで全て実装することを考えたほうが結局開発工数を削減できるかもしれません。
いい判断をするためにも、C++とPythonはどちらも問題なく書けるような技術スタックを持っておいた方が便利だと思います。
###Python, C++ を繋ぐ時にかかってくるオーバーヘッドについて知る
仮に部分的にC++で高速化しようとする結論になったとして、Pythonへのポーティングをすることになると思います。
その際に、もう一度考えてみましょう。
そのポーティング、本当に必要ですか?ということです。
後述しますが、C++からPythonへのポーティングでかかってくるオーバーヘッドは存在し、
想定していたよりかなり大きいことがあります。
そのオーバーヘッドを前もって知ることは難しいかもしれませんが、オーバーヘッドを考慮すると、
- ポーティングなど行わずに全てPythonでやった方が簡単だし無難に速い
- ポーティングなど行わずに全てC++でやった方が工数はかかったが圧倒的に速い
というような未来が予想されるかもしれません。
このあたりはやってみないとわからないことも多いのでなんとも言えませんし、
考えるよりやってみろ、みたいなところもあるのは承知しています。
しかしながら、もし経験があるエンジニアが周りにいるのであれば、
オーバーヘッドやかかってくる工数について相談してアドバイスを仰ぐことも大事だと思います。
###pybind11のポーティングにおける優位性
ポーティングに関して検索すると一番多くヒットするのが
と
をみることになるのではないかと思います。
C++での実装をPythonへポーティングしたい際、多くの場面でpybind11を選択したほうが良い
これはこの記事で伝えたいことことのうちの一つではあるのですが、
pybind11は性能面、実装のしやすさの両方の観点から、Cythonより優秀だと考えています。
逆に、ポーティングを目的とした場合、Cythonの方が必要になる場面はあまりないかなと考えています。
仮にCythonを使うとすれば、
- C++で実装不十分なところがあり、それをCythonによって補完したい場合
- Cythonで実装することに慣れており、pybind11を使うことに抵抗がある場合
というようなシチュエーションが考えられるのかな、と思います。
上以外の場合はあえてCythonを使ってポーティングをする必要はないかなと感じています。
しかしながら上の場合についても、
C++で実装不十分なところがあり、それをCythonによって補完したい場合
このパターンの時は、速度が求められる実装の場合、なるべく実装不十分な箇所をC++側に押し込めるほうが無難かと思います。
不十分である箇所をPythonで実装補完してもいいと思いますし、完全にC++レイヤを下位レイヤ、Pythonレイヤを上位レイヤと切り分けることができるのであれば、その方が良いと思います。
しかしながら、わざわざCythonを使って実装を補完する、C++とPythonの中間レイヤを実装する必要はあまりないかな、と思っています。
コードの保守や改修なども大変になります。
####Cythonで実装することに慣れており、pybind11を使うことに抵抗がある場合
この場合についても、pybind11でのポーティングは非常に簡単であり、後述の実装上でも工数がCythonより優秀だと考えられるところがあることから、思い切ってpybind11を用いるように方向転換しても良いかと思います。
言語として独立性のあるCython
もちろん、CythonはCython言語として独立できるため、Cythonを使うべき場所もあるのはあるかと思います。
実際にC++やPythonで実装せずに、Cythonで全て書くことも可能である、ということです。(それをやる人がいるかは別として、、)
一方でpybind11は名前の通り、C++からpythonへのバインディングに特化したライブラリであり、
pybindでコードを全て書くようなことはできません。
##Python側で上位レイヤを実装しようとした時の注意点
判断の結果、実装にかかる工数や速度要求のことを考慮し
- 低レイヤの実装をC++で一部行い
- それを呼ぶ上位レイヤとしてPythonで実装を行う
ということになったとします。
この時、それによって生まれる可能性のあるのちのちの技術的問題点に関して知っておく必要があります。
###並列化しようとする時の注意点
PythonレイヤからC++で実装されたクラスオブジェクトを扱う際、
C++で実装されたクラスがPickleオブジェクトにできないことに留意しておく必要があります。
これは普通にバイナリとして保存してロードするPickle.dumps, Pickle.loadsなどをするときだけではなく、
concurrent.futures
などの ProcessPoolExecutor
などを用い、並列化を行おうと思った時にも重要です。
基本的に、各プロセスに受け渡すオブジェクトはPickle.dumpsできることを求められるため、
C++で構築したクラスをこの手法で並列化しようとする時にかなり面倒なことになります。
対応策としては、
- C++のクラスをPython側でラップし、そのラップするクラスに関してはうまいことPickle.dumpsができるようにしておく
などもできますが、
私は並列かがあらかじめ必要になるとわかっている関数に関しては、
C++側で前もって並列化で処理するコードを作っておき、
Pythonからはその関数にリストなどを放り込むだけ、にしています。
こうすることでPickle.dumpsなどに関しては考えなくて済みます。
もしバイナリなどにする必要があるにしても、C++側のcerealライブラリなどを使って
C++レイヤでバイナリ化できるように手配しておくと、Python側での実装が最小限のコストで済むためお勧めかなと思います。
###オーバーヘッドにおける注意点
PythonからC++のコードを呼ぼうとしていることには、何かしらの意味があるはずです。
例えば、
- 上位レイヤは実装上汎用性を持たせた形にしたいため、C++で全てを記述することが難しい、
- Pythonのウェブフレームワーク(たとえば Djangoとか)を使っており、そこからC++の部分を呼びたい
などです。このお手軽さを手に入れることができる反面、
- C++からPythonへの型変換オーバーヘッド
はどうしても大きくなることについてきちんと理解しておく必要があります。
特に、実行時間などがシビアになるであろうサービスに関しては、
C++を実行するだけだと圧倒的に早いのにPythonからコールすると時間がかかってしまう、
というような事態になってしまいます。
このオーバーヘッドについては、記述したように、Cythonで実装しようとするよりも、pybind11を用いた方が小さく済むことが多いですが、それでもある程度の時間がかかることは前もって予想しておくべきかと思います。
##pybind11がCythonに比べて優れているところ
筆者がいろいろCythonやpybind11に触れてみて、pybind11の方が優れているな、と思ったことを書いていきます。
C++ のvectorなどに対して、Pythonからnumpy.arrayを入力できる
これに関しては、Cythonを使ってポーティングする時は、内部でリストにして渡す必要があります。
したがって、Cythonレイヤで型を整形するコードさえ書けば、Pythonからnumpy.arrayなどを渡すことも可能ですが、
pybind11に関してはその辺の変換を行ってくれるのでとても便利です。
C++への参照渡しが簡単にできる
Cythonを使ってポーティングする際、例えばC++で vector &x などを受け取るような実装にしていた時、
リストをPythonからそのまま渡すことができません。
**その代わりに、Cythonレイヤにて vector<double>
を cdef
し、それにリストから値をパースするように記述する必要があります。**これがなかなか手間がかかります。
また、この作業は多次元のvectorなどを渡す時には大きなオーバーヘッドになりうるため、注意が必要です。
ビルド時間が短い
Cythonを使ってポーティングした際、それをsetuptools などを使ってsetup.pyに必要なファイルを記述し、
python setup.py install
などを用いて動的ファイルをpython のsite-packageに入れる必要があります。
一方、pybind11を用いる時はC++を用いる時と同じようにCMakeなどを使ってビルドするだけで、動的ファイルをカレントディレクトリに作ってくれるため便利です。また、ビルド時間もかなり早いためデバッグなども捗りやすいです。
同じ分量のC++コードをポーティングする際に、実際にビルド時間は10倍は変わってくる(少なくとも筆者の実装だと)ことも全然ありますし、それ以上変わってくることもあります。
管理するファイルが格段に少なくて済む
Cythonを使ってポーティングした際、実際に実装するものはC++のクラスや関数のラッパーのようなものになるため、
たとえばクラスAをポーティングする際は、A.pxd というヘッダーファイルと、A.pyxという中身を書くファイルを用意し、
ヘッダーファイルにC++からインポートするコード、中身のところに必要な型変換や実際にC++の関数を呼ぶコードを書く必要があります。
一方でpybind11だとメインファイルにポーティングしたいクラスや関数をただ羅列していくことでポーティングが可能です。
実際に複数のクラスのポーティングであっても1ファイルでまとめることは可能ですし、各関数も1行だけでポーティングするため、ファイルの中身がごちゃごちゃもしません。
オーバーロードしている関数をポーティングしやすい
C++側でオーバーロードをするような関数を実装している場合、pybind11::overload_cast を用いることで簡単にポーティングすることができます。
Cythonを使っていた場合はもう少し面倒なことになります。実際にpyxファイルでラップする関数を定義する際に、型を自身でチェックし、つまりif文を使ってどのC++側の関数を呼ぶかを自分で書くことが必要になってきます。
double が入るか、int が入るかというような違いだったり、そもそも引数の数が違うような関数であればそこまでする必要もないですが、vector とvectorをそれぞれ受け取るようなオーバーロードされた関数がC++側にある時などは、
Pythonから見ると入っていくのはどちらもlistであることから、中身の型を判断して、C++側の関数を判断するようなロジックをCythonに書くことになります。これはけっこう面倒になることも多いので、pybind11の方がかなり高数削減できると考えられます。
##pybind11 を用いる時の注意点
上記の通りpybind11は非常に使い勝手が良いですが、C++の実装の仕方によっては注意することも出てきます。
template を多用したクラス構成になっている場合
この場合は、テンプレートを用いたままポーティングはできないため、
テンプレートに対して実際に型を当てはめた後のクラスを作り、それをラッパーとしてC++側で実装することになります。
これは少しめんどくさいかもしれませんが、そのあとにそのラップしたクラスをpybind11によってポーティングすることになります。
テンプレートを大量に使っているコードなどについては注意が必要です。
他にも注意点はあると思うものの、、
書いていて思いましたが、pybind11を使う際に面倒だなと感じることはかなり少なく、templateのところを気をつければ、
オーバーロードや型変形なども容易であるため、あまり難しいと感じることは少ないのかなと思いました。
##まとめ
長々と書いてしまいましたが、結論としては
C++、Pythonの特性をよく活かした実装ができれば嬉しい
ということです。
そのための一つの手段としてポーティング、バインディングがあること、
その際はpybind11を使うことが筆者としてはお勧めなこと。
について理解していただけたら幸いです。
今回はこの辺で。