« VC2005でboost::Pythonを使う | メイン | 出張Shibuya.js 24の発表資料 »

PythonからSIMD(SSE2)を使うC++関数を呼び出す

ここではLL魂2007デモコードで用いたPythonからC++で書かれた関数を呼び出す方法について説明します.

今回は大量の数値演算をするためC++側でSSE2を使うことにしました.
そのためには大前提としてメモリ上に連続して並んだデータが必要になります.
Pythonのリストはそのような用途には向かないため,arrayを使います.
今回は浮動小数を使うのでdoubleの'd'を利用することにしました.精度を落としてよければfloatにして速度を稼ぐこともありです.

vx = array.array('d', [0.0] * N)

次にC++(boost::python)からこのarrayにアクセスするためには,生のポインタ情報が必要になります.
それにはbuffer_info()を使います.マニュアルにはこのAPIは後方互換性のために残されているので使わない方よいと書かれていますが,便利なので使っちゃいました.

vx.buffer_info()[0]

がvxの生のポインタ情報となります(vx.buffer_info()[1]にはlen(vx)が入ります).これをboost::python側に渡します.
ポインタ情報は一度だけ渡しておけば十分なのでchaos.cpp側にPointsというクラスを作り,そのコンストラクタにデータを渡します.


/* Pythonからの呼び出し方法 */

funcz = "x * y - 2.67 * z"
...
points = ChaosByXbyak.Points(N, vx.buffer_info()[0], ..., funcz)

/* C++での受け取り方法 */

class Points {
...
public:
    Points(int n, int vx, ..., const std::string& funcz);
};
BOOST_PYTHON_MODULE(ChaosByXbyak)
{
    class_<Points>("Points", init<int, int, ..., const std::string&>())
        .def("update", &Points::update)
    ;
}

Pythonの文字列はそのままstd::stringで受け取ることができますが,生のポインタはエラーになるのでやや強引ですがintにcastして受けることにしました.
Pointsクラスを作成し,BOOST_PYTHON_MODULE()内でclass_テンプレートを用いてその引数情報と一緒に登録します.簡単ですね.

このようにしてvxのポインタ情報を受け取れればあとは思いのままです.

たとえば整数の配列の値を2倍にするコードは次のようにすればできます.

/* Python側 */

import array
import mylib
a = array.array('l', range(10))
print a
mylib.twice(a.buffer_info()[0], a.buffer_info()[1])
print a

/* C++側 */

#include <boost/python.hpp>
void twice(int ptr, int num)
{
    int *p = reinterpret_cast<int*>(ptr);
    for (int i = 0; i < num; i++) {
        p[i] *= 2;
    }
}
BOOST_PYTHON_MODULE(mylib)
{
    def("twice", twice);
}

実行結果

array('l', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
array('l', [0, 2, 4, 6, 8, 10, 12, 14, 16, 18])

一点注意を述べるとSSE2を使う場合,連続するデータは16byte alignmentされていることが望ましい(vx.buffer_info()[0] % 16 == 0)わけですが,Pythonはそんなことは知りませんのでその条件は一般には満たされません.C++側で注意するしかありません.
回避方法としてたとえばPython側で一つだけ多い配列を確保してC++側でalignmentがずれていれば配列の+1オフセットからデータを使うなどの方法がありますが,やや煩雑です.

アクロバティックな解としては,LL魂2007デモコードの最後に述べた様に,Python自体を改造して16byte alignmentさせる方法もあります.
具体的にはPython/include/pymem.hの70行目あたり,

#define PyMem_MALLOC(n)         malloc((n) ? (n) : 1)
#define PyMem_REALLOC(p, n)     realloc((p), (n) ? (n) : 1)
#define PyMem_FREE        free

#define PyMem_MALLOC(n)         _aligned_malloc(((n) ? (n) : 1), 16)
#define PyMem_REALLOC(p, n)     _aligned_realloc((p), ((n) ? (n) : 1), 16)
#define PyMem_FREE        _aligned_free

にします(上記方法はVC7.1以降のみ).互換性も何もなくなりますが,手っとり早くすませられる(LLの原点?)ので悪くはないでしょう.将来的には本家が対応してくれるかもしれません.

コメントを投稿

(いままで、ここでコメントしたことがないときは、コメントを表示する前にこのブログのオーナーの承認が必要になることがあります。承認されるまではコメントは表示されません。そのときはしばらく待ってください。)