Python による C の関数の呼び出し2016年2月14日 | |
はじめにPython による C の関数の呼び出しについて試してみた。元ネタは Micha Gorelick, Ian Ozsvald 著 "ハイパフォーマンス Python"。 環境
※MinGW にも Python が入っているので、Anaconda のほうを使うようにパスを設定している。 コードサンプルコードは以下の通り。 test.c
#include <stdio.h>
#include <math.h>
#include "test.h"
void test(int n, double a[][2])
{
double dx = 2*M_PI/(n - 1);
int i;
for(i = 0; i < n; i++)
{
double x = i*dx;
double f = sin(x);
a[i][0] = x;
a[i][1] = f;
}
}
test.h #ifndef TEST_H #define TEST_H void test(int n, double a[][2]); #endif /* TEST_H */ Makefile all: gcc -Wall -O2 -c test.c gcc -shared -o test.so test.o x = [0, 2π] の範囲で sin 関数を計算した結果をリストで返すものである。 比較のため、同じ処理を行う Python コードを以下に示す。 call_func.py
from math import pi, sin
def test(n):
dx = 2*pi/(n - 1)
r = []
for i in xrange(n):
x = i*dx
f = sin(x)
r.append((x, f))
return r
Python 関数の実行比較のため、まず Python で書かれた関数の実行速度を計測する。生のコードの結果は以下の通りである。 $ python -m timeit -n 5 -r 5 -s "import call_func" "call_func.test(1000000)" 5 loops, best of 5: 505 msec per loop Numba 版Numba を使ってみる。コードは以下の通り。
from math import pi, sin
from numba import jit
@jit
def test(n):
dx = 2*pi/(n - 1)
r = []
for i in xrange(n):
x = i*dx
f = sin(x)
r.append((x, f))
return r
結果は以下の通り。 $ python -m timeit -n 5 -r 5 -s "import call_func" "call_func.test(1000000)" 5 loops, best of 5: 221 msec per loop Cython 版Cython も試してみる。コードは以下の通り。 call_func.pyx
from math import pi, sin
def test(int n):
cdef double dx
cdef int i
cdef double x
cdef double f
dx = 2*pi/(n - 1)
r = []
for i in xrange(n):
x = i*dx
f = sin(x)
r.append((x, f))
return r
setup.py
from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext
setup(
cmdclass={'build_ext': build_ext},
ext_modules=[Extension("calculate", ["call_func.pyx"])]
)
コンパイル $ python setup.py build_ext --inplace 結果は以下の通り。 $ python -m timeit -n 5 -r 5 -s "import calculate" "calculate.test(1000000)" 5 loops, best of 5: 278 msec per loop Numba のほうが速い。Cython よくわかっていないので、もう少しなんとかなるのかもしれないが、ここでの目的からずれるので、これでよしとする。 Python から C の関数を呼び出すPython から C の関数を呼び出す仕組みとして、ctypes や CFFI がある。 C のライブラリの準備C の関数を呼び出すために、まずライブラリをコンパイルする。GCC だと、次のようにする。 $ gcc -Wall -O2 -c test.c $ gcc -shared -o test.so test.o ctypesctypes は Python の標準モジュールである。次のように用いる。 call_c_func_ctypes.py
import ctypes
_test = ctypes.CDLL('test.so')
TYPE_INT = ctypes.c_int
TYPE_DOUBLE_2 = ctypes.c_double*2
TYPE_DOUBLE_P_2 = ctypes.POINTER(TYPE_DOUBLE_2)
# void test(int n, double a[][2]);
_test.test.argtypes = [
TYPE_INT,
TYPE_DOUBLE_P_2
]
_test.test.restype = None
def test(n):
_n = TYPE_INT(n)
TYPE_DOUBLE_N_2 = TYPE_DOUBLE_2*n
_r = [TYPE_DOUBLE_2(0, 0) for i in xrange(n)]
_r = TYPE_DOUBLE_N_2(*_r)
_test.test(_n, _r)
r = [[elem[i] for i in (0, 1)] for elem in _r]
return r
ctypes.CDLL() でライブラリを読み込んでいる。ctypes.c_int などは、ctypes で定義されている C のデータ型である。"ctypes.c_double*2" は要素を 2 つ持つ配列、ctypes.POINTER(x) は x のポインタを表す。 C の関数を呼び出すために、引数と返り値の型を指定する。_test.test.argtypes に引数の型の配列を、_test.test.restype に返り値の型を入れる。 関数の引数は Python のデータから ctypes のデータに変換する必要がある。ふつうの変数は単純である。
_n = TYPE_INT(n)
配列がややこしい。
_r = [TYPE_DOUBLE_2(0, 0) for i in xrange(n)]
_r = TYPE_DOUBLE_N_2(*_r)
TYPE_DOUBLE_N_2 型 (実体は (c_types.c_double*2)*n) には TYPE_DOUBLE_2 型 (実体は ctypes.c_double*2) の引数を N 個渡す必要があり、TYPE_DOUBLE_2 型には double 型の引数を 2 個渡す必要があるので、まず TYPE_DOUBLE_2 型の要素を N 個持つリストを作り、それを展開して TYPE_DOUBLE_N_2 に渡している。 型変換した引数を用いて関数を呼び出す。
_test.test(_n, _r)
実行結果は以下の通り。 $ python -m timeit -n 5 -r 5 -s "import call_c_func_ctypes" "call_c_func_ctypes.test(1000000)" 5 loops, best of 5: 3.5 sec per loop Python 版の結果の単位は msec だったが、今回の結果は単位が sec になっている。つまり、実行速度がものすごく遅い。C で書けば速度が速くなるかというと、そうでもないということである。この場合は、関数が遅いというより、関数を呼び出す前後のデータの準備・後処理に時間がかかっている。 上記のコードを NumPy を用いて書き直す。 call_c_func_ctypes2.py
import ctypes
import numpy as np
_test = ctypes.CDLL('test.so')
TYPE_INT = ctypes.c_int
TYPE_DOUBLE_2 = ctypes.c_double*2
TYPE_DOUBLE_PP = ctypes.POINTER(ctypes.POINTER(ctypes.c_double))
# void test(int n, double a[][2]);
_test.test.argtypes = [
TYPE_INT,
TYPE_DOUBLE_PP
]
_test.test.restype = None
def test(n):
_n = TYPE_INT(n)
r = np.zeros((n, 2))
_r = r.ctypes.data_as(TYPE_DOUBLE_PP)
_test.test(_n, _r)
return r
今回は NumPy のメモリをそのまま用いているため、データの受け渡しの手間が要らなくなっている。結果は以下の通り。 $ python -m timeit -n 5 -r 5 -s "import call_c_func_ctypes2" "call_c_func_ctypes2.test(1000000)" 5 loops, best of 5: 54.6 msec per loop Python 版より 10 倍ほど速くなった。よかった。返り値の型が変わってしまっているが、使い方はさほど変わらないので、よしとする。 CFFICFFI (C Foreign Function Interface for Python) というモジュールでも、ctypes と同様のことができる。 call_c_func_cffi.py
from cffi import FFI
ffi = FFI()
ffi.cdef(r'''
void test(int n, double a[][2]);
''')
lib = ffi.dlopen('test.so')
def test(n):
_r = ffi.new('double[%d][2]' % n)
lib.test(n, _r)
r = [[elem[i] for i in (0, 1)] for elem in _r]
return r
ctypes よりだいぶスッキリしたコードになっている。関数のシグネチャの情報は、ffi.cdef() で C の関数宣言をそのまま書くことで表現されている。 実行結果は以下の通り。 $ python -m timeit -n 5 -r 5 -s "import call_c_func_cffi" "call_c_func_cffi.test(1000000)" 5 loops, best of 5: 683 msec per loop ctypes ほどではないが、やはり遅い。 NumPy 版は以下の通り。 call_c_func_cffi2.py
from cffi import FFI
import numpy as np
ffi = FFI()
ffi.cdef(r'''
void test(int n, double a[][2]);
''')
lib = ffi.dlopen('test.so')
def test(n):
r = np.zeros((n, 2))
_r = ffi.cast('double(*)[2]', r.ctypes.data)
lib.test(n, _r)
return r
ffi.new() でデータを作る代わりに、NumPy でデータを作り、それをキャストして渡している。返り値のデータコピーがいらなくなっている。結果は以下の通り。 $ python -m timeit -n 5 -r 5 -s "import call_c_func_cffi2" "call_c_func_cffi2.test(1000000)" 5 loops, best of 5: 54.1 msec per loop Python コードの高速化そもそも比較した Python コードは効率が悪いのではないかという気がしてきたので、コードの高速化を図った。 リスト内包表記call_func2.py
from math import pi, sin
def test(n):
dx = 2*pi/(n - 1)
r = [(i*dx, sin(i*dx)) for i in xrange(n)]
return r
結果 $ python -m timeit -n 5 -r 5 -s "import call_func2" "call_func2.test(1000000)" 5 loops, best of 5: 448 msec per loop 少し速くなった。 NumPy の利用call_func_numpy.py
from math import pi, sin
import numpy as np
def test(n):
dx = 2*pi/(n - 1)
r = np.c_[np.arange(n)*dx, np.sin(np.arange(n)*dx)]
return r
結果 $ python -m timeit -n 5 -r 5 -s "import call_func_numpy" "call_func_numpy.test(1000000)" 5 loops, best of 5: 54.1 msec per loop C に匹敵する速度。 まとめ
参考文献 | |
| PENGUINITIS | |