PythonとPython C extensionの速度比較

今回はこのサイトを参考にして、C extensions(C拡張)についての知識を深めてみようと思う。

スポンサーリンク

Writing Python/C extensions by hand

ctypesインターフェイスはすぐに書けるが、必ずしも常にPythonよりも高速であるとは限らない。cos関数をPythonのmathライブラリとlibmをctypesバインディングを介してコールする方法を比べてみる。

import math, time, ctypes

R = range(100000)

libc = ctypes.CDLL("libm.so.6", ctypes.RTLD_GLOBAL)
libc.cos.argtypes = [ctypes.c_double]
libc.cos.restype = ctypes.c_double

def do_timings(cos):
  t1 = time.time()
  for x in R:
    cos(0.0)
  return time.time()-t1

def do_math_cos():
  return do_timings(math.cos)

def do_libc_cos():
  return do_timings(libc.cos)

print ("math.cos", do_math_cos())
print ("libc.cos", do_libc_cos())
math.cos 0.01768660545349121
libc.cos 0.04040241241455078

上記の結果が、ctypes使用のオーバーヘッドが、通常のPython/C extensionをコールするよりも約3倍の時間を要することを示している。

Drawing a Mandelbrot set

お馴染みのマンデルブロ集合描画をやる。以下にそのソースコードお載せる。

#include "Python.h"

/* make this static if you don't want other code to call this function */
/* I don't make it static because want to access this via ctypes */
/* static */
int iterate_point(double x0, double y0, int max_iterations) {
	int iteration = 0;
	double x=x0, y=y0, x2=x*x, y2=y*y;
	
	while (x2+y2<4 && iteration<max_iterations) {
		y = 2*x*y + y0;
		x = x2-y2 + x0;
		x2 = x*x;
		y2 = y*y;
		iteration++;
	}
	return iteration;
}

/* The module doc string */
PyDoc_STRVAR(mandelbrot__doc__,
"Mandelbrot point evalutation kernel");

/* The function doc string */
PyDoc_STRVAR(iterate_point__doc__,
"x,y,max_iterations -> iteration count at that point, up to max_iterations");

/* The wrapper to the underlying C function */
static PyObject *
py_iterate_point(PyObject *self, PyObject *args)
{
	double x=0, y=0;
	int iterations, max_iterations=1000;
	/* "args" must have two doubles and may have an integer */
	/* If not specified, "max_iterations" remains unchanged; defaults to 1000 */
	/* The ':iterate_point' is for error messages */
	if (!PyArg_ParseTuple(args, "dd|i:iterate_point", &x, &y, &max_iterations))
		return NULL;
	/* Verify the parameters are correct */
	if (max_iterations < 0) max_iterations = 0;
	
	/* Call the C function */
	iterations = iterate_point(x, y, max_iterations);
	
	/* Convert from a C integer value to a Python integer instance */
	return PyInt_FromLong((long) iterations);
}

/* A list of all the methods defined by this module. */
/* "iterate_point" is the name seen inside of Python */
/* "py_iterate_point" is the name of the C function handling the Python call */
/* "METH_VARGS" tells Python how to call the handler */
/* The {NULL, NULL} entry indicates the end of the method definitions */
static PyMethodDef mandelbrot_methods[] = {
	{"iterate_point",  py_iterate_point, METH_VARARGS, iterate_point__doc__},
	{NULL, NULL}      /* sentinel */
};

/* When Python imports a C module named 'X' it loads the module */
/* then looks for a method named "init"+X and calls it.  Hence */
/* for the module "mandelbrot" the initialization function is */
/* "initmandelbrot".  The PyMODINIT_FUNC helps with portability */
/* across operating systems and between C and C++ compilers */
PyMODINIT_FUNC
initmandelbrot(void)
{
	/* There have been several InitModule functions over time */
	Py_InitModule3("mandelbrot", mandelbrot_methods,
                   mandelbrot__doc__);
}

上のコードをコンパイルするsetup.pyを下に示す。

from distutils.core import setup, Extension

setup(name="mandelbrot", version="0.0",
	ext_modules = [Extension("mandelbrot", ["mandelbrot.c"])])

上のコードを実行してmandelbrot.cをコンパイルする。

!python setup.py build
running build
running build_ext
building 'mandelbrot' extension
creating build
creating build/temp.linux-x86_64-3.6
/root/.pyenv/versions/miniconda3-4.3.30/envs/caffe2/bin/x86_64-conda_cos6-linux-gnu-cc -DNDEBUG -fwrapv -O2 -Wall -Wstrict-prototypes -march=nocona -mtune=haswell -ftree-vectorize -fPIC -fstack-protector-strong -fno-plt -O2 -pipe -DNDEBUG -D_FORTIFY_SOURCE=2 -O2 -fPIC -I/root/.pyenv/versions/caffe2/include/python3.6m -c mandelbrot.c -o build/temp.linux-x86_64-3.6/mandelbrot.o
mandelbrot.c: In function 'py_iterate_point':
mandelbrot.c:46:9: warning: implicit declaration of function 'PyInt_FromLong'; did you mean 'PyLong_FromLong'? [-Wimplicit-function-declaration]
  return PyInt_FromLong((long) iterations);
         ^~~~~~~~~~~~~~
         PyLong_FromLong
mandelbrot.c:46:9: warning: return makes pointer from integer without a cast [-Wint-conversion]
  return PyInt_FromLong((long) iterations);
         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
mandelbrot.c: In function 'initmandelbrot':
mandelbrot.c:68:2: warning: implicit declaration of function 'Py_InitModule3'; did you mean 'Py_Initialize'? [-Wimplicit-function-declaration]
  Py_InitModule3("mandelbrot", mandelbrot_methods,
  ^~~~~~~~~~~~~~
  Py_Initialize
mandelbrot.c:70:1: warning: control reaches end of non-void function [-Wreturn-type]
 }
 ^
creating build/lib.linux-x86_64-3.6
x86_64-conda_cos6-linux-gnu-gcc -pthread -shared -Wl,-O2 -Wl,--sort-common -Wl,--as-needed -Wl,-z,relro -Wl,-z,now -Wl,-rpath,/root/.pyenv/versions/miniconda3-4.3.30/envs/caffe2/lib -L/root/.pyenv/versions/miniconda3-4.3.30/envs/caffe2/lib -Wl,-O2 -Wl,--sort-common -Wl,--as-needed -Wl,-z,relro -Wl,-z,now -Wl,-rpath,/root/.pyenv/versions/miniconda3-4.3.30/envs/caffe2/lib -L/root/.pyenv/versions/miniconda3-4.3.30/envs/caffe2/lib -Wl,-O2 -Wl,--sort-common -Wl,--as-needed -Wl,-z,relro -Wl,-z,now -march=nocona -mtune=haswell -ftree-vectorize -fPIC -fstack-protector-strong -fno-plt -O2 -pipe -DNDEBUG -D_FORTIFY_SOURCE=2 -O2 build/temp.linux-x86_64-3.6/mandelbrot.o -o build/lib.linux-x86_64-3.6/mandelbrot.cpython-36m-x86_64-linux-gnu.so
import ctypes
import mandelbrot

ctypes_iterate_point = ctypes.CDLL("mandelbrot.so").iterate_point
ctypes_iterate_point.restype = ctypes.c_int
ctypes_iterate_point.argtypes = [ctypes.c_double, ctypes.c_double, ctypes.c_int]

x = -2
y = -1
w = 2.5
h = 2.0

NY = 40
NX = 70
RANGE_Y = range(NY)
RANGE_X = range(NX)

def render(iterate_point):
	chars = []
	append = chars.append
	for j in RANGE_Y:
		for i in RANGE_X:
			it = iterate_point(x+w/NX*i, y+h/NY*j, 1000)
			if it == 1000:
				append("*")
			elif it > 5:
				append(",")
			elif it > 2:
				append(".")
			else:
				append(" ")
		append("\n")
	return "".join(chars)

import time
t1 = time.time()
s1 = render(mandelbrot.iterate_point)
t2 = time.time()
s2 = render(ctypes_iterate_point)
t3 = time.time()
assert s1 == s2
print (s1)
print ("as C extension", t2-t1)
print ("with ctypes", t3-t2)
---------------------------------------------------------------------------
ImportError                               Traceback (most recent call last)
<ipython-input-19-db7a4ceb6187> in <module>()
      1 import ctypes
----> 2 import mandelbrot
      3 
      4 ctypes_iterate_point = ctypes.CDLL("mandelbrot.so").iterate_point
      5 ctypes_iterate_point.restype = ctypes.c_int

ImportError: dynamic module does not define module export function (PyInit_mandelbrot)

これはCソースコードがpython2用に書かれているのが原因で、このエラーを解消するにはコードの書き換えが必要になるが、これの習性については後日改めてやるつもりでいる。