Python
C++
pybind11

[python高速化]pybind11によるc++組み込み

この記事について

pythonを高速化する方法の一つに、重い処理の部分だけC/C++でライブラリを作ってしまうというやり方があります。
pythonにC++の関数やクラスを組み込むにあたってpybind11が便利そうなので試してみたのですが、実行できるようになるまで思っていたより情報が少なく手間取ったので、メモを残しておきます。

参考リンク

pybind11自体の説明などは以下のサイトが参考になります。

公式ドキュメント
pybind11でC++の関数をpythonから使う
PythonからC++のクラスを使ってみるテスト(pybind11版)

インストール

pipを使う場合

(2017/11/19追記)
pipでインストールできるようになりました。

pip install pybind11

ソースが必要な場合

pipでインストールした場合は下記は不要です。

pybind11はヘッダのみで利用できるので、インストールは不要です。ソースコードをgit cloneするかダウンロードして、ヘッダにパスを通せばOKです。

cd <インストール先ディレクトリ>
git clone https://github.com/pybind/pybind11.git

サンプルコードとコンパイル方法

サンプルコード

以下はサンプルコードです。簡単にですが、関数、クラス、vectorの使い方を示しています。
基本的には、通常のc++の関数やクラスを定義しておいた上で、PYBIND11_MODULEというところで、pythonから使いたい関数やクラスを登録します。これだけで、pythonコード上でimportして使えるようになります。

vectorは自動でリストと相互変換してくれるようです。ただし、vectorを利用するには<pybind11/stl.h>のインクルードが必要になります。

mylibs.h
using namespace::std;

int add(int x, int y);

vector<int> vec_double(vector<int> &v);

vector<vector<int>> vec_add(vector<vector<int>> &vec);

class POINT {
private:
    int x;
    int y;
public:
    int sum;

    POINT(int x, int y) { this->x = x; this->y = y; this->sum = x+y; }
    int X() { return x; }
    int Y() { return y; }
};

POINT move_p(POINT p, int d);
mylibs.c
#include <pybind11/pybind11.h>
#include <pybind11/stl.h> // vector用
#include "mylibs.h"

int add(int x, int y) {
    return x+y;
}

vector<int> vec_double(vector<int> &vec) {
    for(auto &v : vec) {
        v *= 2;
    }
    return vec;
}

vector<vector<int>> vec_add(vector<vector<int>> &vec) {
    vector<vector<int>> result(vec.size(), vector<int>());
    for(int i = 0; i < vec.size(); i++) {
        int tmp = 0;
        for(auto &t : vec[i]) {
            tmp += t;
            result[i].push_back(tmp);
        }
    }
    return result;
}

POINT move_p(POINT p, int d) {
    return POINT(p.X() + d, p.Y() + d);
}

namespace py = pybind11;
PYBIND11_PLUGIN(mylibs) {
    py::module m("mylibs", "mylibs made by pybind11");
    m.def("add", &add);
    m.def("vec_double", &vec_double);
    m.def("vec_add", &vec_add);

    py::class_<POINT>(m, "POINT")
        .def_readwrite("sum", &POINT::sum)
        .def(py::init<int, int>())
        .def("X", &POINT::X)
        .def("Y", &POINT::Y);
    m.def("move_p", &move_p);

    return m.ptr();
}

コンパイル方法

pipインストールした場合

(2017/11/19追記)
pybind11をpipでインストールした場合はコンパイルが簡単になります。
公式ドキュメント (Build systems - Building manually)

clang++ -O3 -Wall -shared -std=c++14 -fPIC `python3 -m pybind11 --includes` mylibs.cpp -o mylibs`python3-config --extension-suffix`

git cloneした場合

pipでインストールせずにソースコードをgit cloneした場合は以下のようになります。

試した環境
OS: MacOS X (Sierra 10.12.6)
コンパイラ: clang (Apple LLVM version 9.0.0 (clang-900.0.37))
python: Anaconda Python3 (Python 3.6.0 :: Anaconda custom (x86_64))**

clang++ -std=c++14 \
  -shared \
  -I<git cloneしたディレクトリ>/pybind11/include \
  -L/<Anacondaインストールディレクトリ>/lib/ \
  `/<Anacondaインストールディレクトリ>/bin/python3-config --cflags --ldflags | sed 's/[^ ]*-stack_size[^ ]*//'` \
  -o mylibs.so \
  mylibs.cpp

ubuntu on Docker版 (2017/11/12追記)
5行目が少しだけ変わっています

clang++ -std=c++14 \
  -shared \
  -I<git cloneしたディレクトリ>/pybind11/include \
  -L/<Anacondaインストールディレクトリ>/lib/ \
  `/<Anacondaインストールディレクトリ>/bin/python3-config --cflags --ldflags` -fPIC \
  -o mylibs.so \
  mylibs.cpp

オプションについて説明すると
-shared : ライブラリ作成
-I : pybind11のインクルードパスを指定
-L : python用のライブラリパスを指定 (python3-configが相対パスしか出力しないため必要となる)
python3-config : python向けライブラリをコンパイルするときのオプションを出力するコマンド
-o : ライブラリのファイル名指定

python3-configについて

  • pyenvやAnacondaなどシステムと異なるpythonを使っている場合には、python3-config/python-configを絶対パスで指定したほうが無難です。何も指定しないとシステムのpython用の結果を出力するので、バージョンが合わないなどのエラーが出ます。
  • 筆者の環境では--ldflagsの出力をそのまま使うと、stack_sizeはmain関数のあるプログラムしか指定できないという旨のエラーが出たので、sedでstack_sizeのオプションを消しています。

警告 'pybind11_init' is deprecated

上記のオプションだけだとpybind11_initの定義が複数回あるという警告が出ます。探してみるとgccのバグだという情報が出てきますが、clangでも警告が出るようです。ただし、警告がでていても正しく動いたので、とりあえずは大丈夫かと思います。

pythonからの呼び出し

pythonから呼び出すサンプルコードは以下のようになります。

test.py
import mylibs

print(mylibs.add(3,5))

p = mylibs.POINT(3,5)
print(p.X(), p.Y(), p.sum)

q = mylibs.move_p(p, 10)
print(q.X(), q.Y(), q.sum)

print(mylibs.vec_double([1,2,4,5]))

print(mylibs.vec_add([
        [1,1,1],
        [1,2,3,4,5],
        [1,-1,1,-1,1,-1],
        [1,2,4,8,16,32,64]
    ]))

# 出力結果
# 8
# 3 5 8
# 13 15 28
# [2, 4, 8, 10]
# [[1, 2, 3], [1, 3, 6, 10, 15], [1, 0, 1, 0, 1, 0], [1, 3, 7, 15, 31, 63, 127]]

呼び出す側は特に難しいことはありません。
二次元のvectorも問題なくリストと相互変換できます。