Python笔记:外部c函数调用
序言
前些时候,一个朋友突然问我:python做计算实在是太慢了,有什么办法可以加速python的运算吗?我说:简单啊,你直接调用外部c函数就行了,我印象中cython可以直接实现的。闻言,我那个朋友喜出望外,遂言:太好了,那你给我写个demo呗。。。
emmmm。。。
好吧,我承认我之前事实上只是知道可以这么做,真的要说实现。。。
唉,自己挖的坑,流着泪也要把它填平了。于是,趁着周末两天,我网上找了一些demo,然后自己实现了几种python调用外部c函数的实现方式。
不要问我为啥今天才发出来,问就是打字慢。
下面,话不多说,上干货!
1. ctypes实现
c_types实现大约是最简单的外部c函数实现方法了,你只需要准备写好你的C函数实现,然后编译,最后调用就行了,无需任何中间文件,一切都是如此简单。
下面,我们以a到b的连续求和函数作为例子来进行说明:
-
step 01: 写作c代码实现
我们在
my_func.c
脚本中实现我们的代码如下:int myfunc(int a, int b){ int ans = 0; for(int i=a; i<=b; ++i){ ans += i; } return ans; }
-
step 02: 编译脚本
我们调用如下命令即可:
gcc -shared -o mylib.so myfunc.c
-
step 03: 调用测试
最后,我们只要在python代码中使用如下方法调用即可:
from ctypes import CDLL mylib = CDLL("./mylib.so") mylib.myfunc(0, 100) # 5050
完事,收工,一切都是如此顺利!
2. cython实现
较之ctypes实现方法,cython方法会更加复杂一点,它不需要依赖于ctypes库,而是直接将c代码转译为python底层c实现中可读的代码,而后将这一部分封装为一个动态链接库。
因此,在这种情况下,我们完全可以将这个生成的动态链接库当成一个普通的python包来进行调用,其执行效率上也会优于ctypes方式的调用。
下面,我们来考察其具体实现。
-
step 01: 准备c函数实现
这部分完全同上,这里不再赘述。
-
step 02: 配置动态链接库
我们在外准备一个
mylib.pyx
文件用于配置我们需要封装的动态链接库。cdef extern from "myfunc.c": cpdef int myfunc(int a, int b)
-
step 03: 动态链接库生成
在准备好了动态链接库配置文件之后,我们构建一个
setup.py
文件来生成我们的动态链接库。setup.py
文件内容如下:from distutils.core import setup, Extension from Cython.Build import cythonize ext_modules=[ Extension("mylib", sources=["mylib.pyx"]) ] setup( name = 'wrapper for myfunc', ext_modules = cythonize(ext_modules) )
而后,我们调用下述命令进行调用:
python setup.py build_ext --inplace
操作完成之后,我们即可得到一个
.so
动态链接库文件,该文件即为我们可调用的python包。 -
step 04: 调用测试
我们使用如下代码即可进行调用测试。
from mylib import myfunc myfunc(0, 100) # 5050
由是,cython方法搞定收工!
3. c extension实现
注意到,cython方式构建动态链接库过程中,会调用cythonize
函数,而这个函数会先生成一个.c
中间文件,而这个中间文件即为我们的动态链接库中真实包含的c函数代码实现。
事实上,后续的setup函数就是针对这个.c
中间文件进行编译并构建为动态链接库。
因此,我们可以绕过cythonize
函数,直接自己来构建这个.c
文件,然后进行动态链接库的构建。
而这,就是c extension方法的主要思路。
同样的,给出其操作流程如下:
-
step 01: c函数准备
这部分内容见上即可。
-
step 02: 配置动态链接库
这里,我们直接给出动态链接库的配置代码示例如下:
// mylib.c #include <Python.c> #include "myfunc.c" // 自定义c函数库,包含目标函数myfunc // 转义c函数 static PyObject * demo(PyObject *self, PyObject *args){ int a, b; if (!PyArg_ParseTuple(args, "ii", &a, &b)) return NULL; return Py_BuildValue("i", f2(a, b)); } // 定义动态链接库中包含的函数 static PyMethodDef DemoMethods[] = { {"myfunc", demo, METH_VARARGS, "my function"}, {NULL, NULL, 0, NULL} }; // 定义动态链接库 static struct PyModuleDef cModPyDem = { PyModuleDef_HEAD_INIT, "mylib", // 导出后的动态链接库名称 "", -1, DemoMethods }; // 构建动态链接库函数接口,后续的setup.py脚本将会调用这一函数进行生成 PyMODINIT_FUNC PyInit_mylib(void){ return PyModule_Create(&cModPyDem); }
-
step 03: 动态链接库构建
动态链接库的构建方法与上述cython方法相仿,我们只需要给出一个
setup.py
脚本并进行调用即可。给出其内容如下:
from distutils.core import setup, Extension module = Extension("mylib", sources=["mylib.c"]) setup(name = "build my c library", ext_modules=[module])
而后,我们同样采用下述命令编译即可:
python setup.py build_ext --inplace
-
step 04: 调用测试
由于这一方式与上述cython方式本质上是完全相同的,因此,其调用方法也完全相同,我们对此不再赘述。
综上,c extension方法搞定!
4. swig实现
swig也是常用的python调用外部c函数的实现方法之一,其核心与上述cython完全相似,唯一的区别点在于,cython方法使用cython库来进行代码转义,而这里使用swig进行代码转义。。。
我们给出其操作流程如下:
-
step 01: c函数实现
bala,bala,bala。。。
-
step 02: 配置动态链接库
使用swig进行动态链接库的配置所需要提供的配置文件为
.i
文件,其内容格式如下:%module mylib %{ #define SWIG_FILE_WITH_INIT #include "myfunc.c" %} int myfunc(int a, int b);
而后,我们调用下述命令即可生成实际的
.c
动态链接库配置脚本:swig -python mylib.i
-
step 03: 构建动态链接库
在上述步骤后,我们会得到一个名为
mylib_wrap.c
的文件,而后,我们仿照c extension方法给出下述构建脚本setup.py
即可:from distutils.core import setup, Extension module = [ Extension('mylib', sources=['myfunc.c', 'mylib_wrap.c']) ] setup (name = 'mylib', ext_modules = module)
执行如下编译命令即可得到最终的
.so
动态链接库文件:python setup.py -build_ext --inplace
-
step 04: 调用测试
da…da…da…
至此,swig方法搞定!
5. 效果测试 & 结论
现在,我们来比较一下上述各个方法调用外部c函数的性能。
我们重复执行 1 0 7 10^7 107次计算1到100的连续求和,得到各个方法的耗时如下:
方法 | 耗时 |
---|---|
python | ~50s |
ctypes | ~7s |
cython | 1.88s |
c extension | 2.76s |
swig | 2.41s |
结论:
- 上述4种方式实现c函数外部调用确实能给python带来极大的性能提升;
- 就实现方式来说,ctypes是最容易实现的,但是相对的,其执行效率也是4种方法中最慢的;
- c extension、cython以及swig三种实现方法本质上来说是同一种实现方法,其外部c函数调用的执行速度上没有量级上的差异,但是从其实际的效果来看,cython方式相对而言操作更为简单,其效率也是最高的。
参考文献
[1] 在python里调用C函数的三种方式
[2] python调用c和c++库(直接调用和使用swig)
[3] SWIG and Python