Pythonのネイティブコード呼び出しのコスト
Pythonでネイティブコードを簡単に呼ぶ方法として、ctypesとboost.pythonがある。ctypesは最近はPython本体に含まれるし、boost.pythonもBoost本体に含まれているようだ。この2つは、swigや自力でインタフェースを書く場合と比べてだいぶ楽に呼び出せる。
で、どのくらいコストが違うのか、試してみようと。ネイティブコードを呼んだ先では大差ないと思うけど、ネイティブコード呼び出しにかかるコストによっては、インタフェースの粒度を考えないといけなくなる。
そこで、ためしに
- 何もしない
- math.sinを呼ぶ
- cmath.sinを呼ぶ
- boost.python経由でstd::sinを呼ぶ
- ctypesでlibmのsinを呼ぶ
という呼び出しの速度を測ってみた。cmathは複素数に対応した数学関数のパッケージです。sinが複素数を返すことはない…と思いますけど、mathとcmathの比較のために一応。200万回の呼び出して時間を測定して平均した。測定環境がVista(32bit)環境のVMWareの中のFedora8(x68_64)環境、というのがややこしい。こういう環境は測定が正確じゃないかもしれないですね。仮想環境のgettimeofdayはあんまり信用できないような気がする。vmware-toolsを入れて時刻をホストと同期はさせてますが。
測定 | 呼び出しに要した時間(us) |
何もしない | 0.535881 |
math.sin | 0.505848 |
cmath.sin | 0.632371 |
boost.python経由でstd::sin | 0.541841 |
ctypesでlibmのsin | 1.307253 |
何もしない、というのは測定したいもの以外の時間(random.randomを読んだりループを回したりしているんで…)だけど、math.sinが異常に高速で、何もしないよりも短い時間になってしまった。それでも「何もしない」を引くと、次のように。
測定 | 呼び出しに要した時間(us) |
math.sin | -0.030033 |
cmath.sin | 0.096490 |
boost.python経由でstd::sin | 0.005960 |
ctypesでlibmのsin | 0.771372 |
ctypesが若干遅いですね。CやC++のコードを1行も書かずにPythonだけでネイティブコードを呼び出せるのは大きなメリットですが、細かい粒度の関数を呼びたい場合はboost.pythonで書いたほうがよさそうな気がする。でも10us程度かかる処理以上のものであれば、ctypesでもいいんじゃないかと思います。これってかなり敷居が低いような気が…
サンプルコードは以下の通り。
boost.pythonからstd::sinを呼ぶもの。
#include <boost/python.hpp>
#include <cmath>
using namespace boost::python;
double sin(double arg){
return std::sin(arg);
}
BOOST_PYTHON_MODULE(mymath){
def("sin", sin);
}
// g++ -O9 -I/usr/include/python2.5 -shared -o mymath.so mymath.C -lboost_python -fPIC
pythonの測定コード。これもけっこういい加減です。
#! /usr/bin/python
import os,sys
import random,time
import ctypes
import ctypes.util
class bench:
def __init__(self):
random.seed()
pass
def bench(self, runs):
st=time.time()
x=self.bench_each(runs)
en=time.time()
d=en-st
print "%d runs %f sec (%f iter/usec, %f usec/iter) %s" %(runs, d, runs/(d*1000000.0), d*1000000.0/runs, self)
def bench_each(self, runs):
for i in xrange(runs):
self.myfunc(random.random())
def myfunc(self, arg):
return arg
class bench_mymath(bench):
def __init__(self):
import mymath
bench.__init__(self)
self.myfunc=mymath.sin
class bench_cmath(bench):
def __init__(self):
import cmath
bench.__init__(self)
self.myfunc=cmath.sin
class bench_math(bench):
def __init__(self):
import math
bench.__init__(self)
self.myfunc=math.sin
class bench_ctypes(bench):
def __init__(self, libname="m"):
bench.__init__(self)
name=ctypes.util.find_library(libname)
self.mylib=ctypes.cdll.LoadLibrary(name)
self.myfunc=self.mylib.sin
self.myfunc.argtypes=[ctypes.c_double]
self.myfunc.restype=ctypes.c_double
bb=bench()
bm=bench_math()
bM=bench_cmath()
bmy=bench_mymath()
bc=bench_ctypes()
N=2000000
for i in [bb,bm,bM,bmy,bc]:
i.bench(N)
一応ファイルでも置いておきます。