メイン

Python アーカイブ

2007年08月06日

LL魂2007デモコード

LL魂2007(LLSpirit)のライトニングトークで発表したときに使ったデモのコード(Python+Xbyak)を公開します.

download

使い方は添付のreadme.txtを参照してください.
ベンチマーク結果をあげておきます.

速度比(小さいほど速い)
ループ回数 実行時間(sec) 処理時間比(Xを1とする)
Xbyak(可変) 100000 12.14 1
python(可変) 100 10.73 894
psyco(可変) 1000 40.8 340
python(固定) 1000 42.55 354
psyco(固定) 10000 32.91 27.4
C++(固定) 100000 13.94 1.16
修正python* + Xbyak 100000 8.75 0.72(参考値)

psycoはPythonのJIT実行環境です.Xbyakはこれの特別版と考えればよいでしょう.psycoは何もしなくても速くなりますが,Xbyakは全て自分でコードを書く必要があります.

今回のベンチマークの目的は,関数を文字列として与えられたときに,それを実行時に評価(eval)する速度比を測ることです.Xbyakやpython(可変), psyco(可変)がそれにあたります.
それに対して,evalがどれぐらい重いのかを見るために,同時に関数を文字列でなく本来の関数として定義した場合の速度も測りました.それがpython(固定)やpsyco(固定),およびC++(固定)で作ったものです.

見てのとおり,可変と固定のハンディをものともせずXbyakがトップです.実は100倍どころの速度比ではありません.通常最速と思われるC++(固定)よりも速かったのは意外です.ただし,これはXbyakでSSE2を使ったためと思われます(でないと勝てるわけがない).

またpsyco(固定)の速度向上も目を見張るものがあります.何もせずにこれだけ速くなるのはたいしたものです.

# Pythonのarrayが16byte alignmentしてくれればXbyak版ももっと速くできる*のですが….

なお,コードを見ればわかりますが,Xbyak版はかなり手抜き(100行程度だし)であること,また一般的に上記のような性能向上が常に得られるとは限りませんが,ピンポイントで効果的に使えればかなりの破壊力があることは分かると思います.

コード内容については後日簡単に説明する予定です.

*おまけ(毒を食らわば皿まで)
Python本体ソースのmalloc関数たちを_aigned_mallocに変更してリコンパイルし,xbyak側のメモリアクセスをalignmentされていないときでも動くmovupsからalignment前提のmovapsに変更すると12sec => 8.75secとなり,効果があることが分かりました.

2007年08月08日

VC2005でboost::Pythonを使う

VC(Visual Studio)2005でboost::pythonを使おうとすると,

error: Python was built with Visual Studio 2003;
extensions must be built with a compiler than can generate compatible binaries.
Visual Studio 2003 was not found on this system. If you have Cygwin installed,
you can try compiling with MingW32, by passing "-c mingw32" to setup.py.

というメッセージがでて使うことができません.

完全無保証ですが,次の方法でとりあえず使うことができました.
# py2exeはまだ動かせてません.どなたか教えてください.

1. PythonをVC2005でコンパイルする.

頑張ってください:-)
本家のソースだけでも最小限の構成ならバイナリを作ることができます.

2. VC2005用boost::pythonをインストールする.

boost/libs/python/build

>set PYTHON_VERSION=2.5
>set PYTHON_PATH=<Pythonのディレクトリ>
>bjam
でboost_python-vc80-mt-1_34.dllを作り,Python25のディレクトリにコピーしておきます.

3. python25/lib/distutilsを次のように変更する.
各自のインストール環境によって変わるかもしれません.このあたりを直せば動くというポインタ情報程度の認識でお願いします.

128行目あたり

-    if version > 7.0:
+    if version >= 8.0:
+        self.set_macro("FrameworkSDKDir", net, "sdkinstallrootv2.0")
+    elif version > 7.0:
        self.set_macro("FrameworkSDKDir", net, "sdkinstallrootv1.1")

255行目あたり

-    if os.environ.has_key("DISTUTILS_USE_SDK") and os.environ.has_key("MSSdk") and self.find_exe("cl.exe"):
+    if self.__version >= 8 or (os.environ.has_key("DISTUTILS_USE_SDK") and os.environ.has_key("MSSdk") and self.find_exe("cl.exe")):

299行目あたり

+    if self.__version >= 8:
+        self.compile_options = [ '/nologo', '/Ox', '/MD', '/W3', '/EHsc' ,
+                                 '/DNDEBUG']
+        self.compile_options_debug = ['/nologo', '/Od', '/MDd', '/W3', '/EHsc',
+                                      '/Z7', '/D_DEBUG']
+    else:
        self.compile_options = [ '/nologo', '/Ox', '/MD', '/W3', '/GX' ,
                                 '/DNDEBUG']
        self.compile_options_debug = ['/nologo', '/Od', '/MDd', '/W3', '/GX',
                                      '/Z7', '/D_DEBUG']

このようにすれば
LL魂2007デモコードのchaos.zipを

setup.py install
でコンパイル&インストールすることができます(VPythonは別途必要).

2007年08月20日

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の原点?)ので悪くはないでしょう.将来的には本家が対応してくれるかもしれません.

About Python

ブログ「mitsunari@cybozu labs」のカテゴリ「Python」に投稿されたすべてのエントリーのアーカイブのページです。過去のものから新しいものへ順番に並んでいます。

前のカテゴリはMD5です。

次のカテゴリはSHA-1です。

他にも多くのエントリーがあります。メインページアーカイブページも見てください。