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)

一応ファイルでも置いておきます。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です