返回介绍

建议91:使用 Cython 编写扩展模块

发布于 2024-01-30 22:19:09 字数 4993 浏览 0 评论 0 收藏 0

Python-API让大家可以方便地使用C/C++编写扩展模块,从而通过重写应用中的瓶颈代码获得性能提升。但是,这种方式仍然有几个问题让Pythonistas非常头疼:

1)掌握C/C++编程语言、工具链有巨大的学习成本,如果没有这方面的技术积累,就无法快速编写代码,解决性能瓶颈。

2)即便是C/C++熟手,重写代码也有非常多的工作,比如编写特定数据结构、算法的C/C++版本,费时费力还容易出错。

所以整个Python社区都在努力实现一个“编译器”,它可以把Python代码直接编译成等价的C/C++代码,从而获得性能提升。通过开发人员的艰苦工作,涌现出了一批这类工具,如Pyrex、Py2C和Cython等。而从Pyrex发展而来的Cython是其中的集大成者。

Cython通过给Python代码增加类型声明和直接调用C函数,使得从Python代码中转换的C代码能够有非常高的执行效率。它的优势在于它几乎支持全部Python特性,也就是说,基本上所有的Python代码都是有效的Cython代码,这使得将Cython技术引入项目的成本降到最低。除此之外,Cython支持使用decorator语法声明类型,甚至支持专门的类型声明文件,以使原有的Python代码能够继续保持独立,这些特性都使它得到广泛应用,如PyAMF、PyYAML等库都使用它编写自己的高效率版本。

安装Cython非常简单,使用pip能够很方便地安装。

pip install 
–U cython

编译时间有点漫长,稍作等待,Cython就自动安装好了。然后我们可以尝试拿之前的arithmetic.py尝试一下,执行命令cython arithmetic.py,很快就完成了,但其实生成了一个arithmetic.c文件,它非常巨大,大概会有两三千行。是的,你没有看错,只有8行有效代码的arithmetic.py文件生成的C代码有两三千行。它的部分代码(subtract函数对应的代码的一分部)如下:

...
/* Python wrapper */
static PyObject *__pyx_pw_10arithmetic_7subtract(PyObject *__pyx_self, PyObject
  *__pyx_args, PyObject *__pyx_kwds); /*proto*/
static PyMethodDef __pyx_mdef_10arithmetic_7subtract = {__Pyx_NAMESTR("subtract"),
  (PyCFunction)__pyx_pw_10arithmetic_7subtract, METH_VARARGS|METH_KEYWORDS, __Pyx_DOCSTR(0)};
static PyObject *__pyx_pw_10arithmetic_7subtract(PyObject *__pyx_self, PyObject
  *__pyx_args, PyObject *__pyx_kwds) {
  PyObject *__pyx_v_x = 0;
  PyObject *__pyx_v_y = 0;
  int __pyx_lineno = 0;
  const char *__pyx_filename = NULL;
  int __pyx_clineno = 0;
  PyObject *__pyx_r = 0;
  __Pyx_RefNannyDeclarations
  __Pyx_RefNannySetupContext("subtract (wrapper)", 0);
  {
  static PyObject **__pyx_pyargnames[] = {&__pyx_n_s__x,&__pyx_n_s__y,0};
  PyObject* values[2] = {0,0};
...

看不懂?没有关系,机器生成的代码本来就不是为了给人看的,还是把它交给编译器吧。

$ gcc -shared -pthread -fPIC -fwrapv -O2 -Wall -fno-strict-aliasing \
 -I/usr/include/python2.7 -o arithmetic.so arithmetic.c

又是一阵等待,编译、链接工作完成后,arithmethic.so文件就生成了。这时候可以像 import普通的Python模块一样使用它。

$ python
>>> import arithmetic
>>> arithmetic.subtract(2, 1)
1

每一次都需要编译、等待未免麻烦,所以Cython很体贴地提供了无需显式编译的方案:pyximport。只要将原有的Python代码后缀名从.py改为.pyx即可。

$ cp arithmetic.py arithmetic.pyx
$ cd ~
$ python
>>> import arithmetic
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: No module named arithmetic
>>> import pyximport; pyximport.install()
(None, <pyximport.pyximport.PyxImporter object at 0x10fbd05d0>)
>>> import arithmetic
>>> arithmetic.__file__
'/Users/apple/.pyxbld/lib.macosx-10.8-x86_64-2.7/arithmetic.so'

从__file__属性可以看出,这个.pyx文件已经被编译链接为共享库了,pyximport的确方便啊!掌握了Cython的基本使用方法之后,就可以更进一步学习了。接下来要谈的是如何通过Cython把原有代码的性能提升许多倍,是的,Cython就是这么快!

在GIS中,经常需要计算地球表面上两点之间的距离。

import math
def great_circle(lon1,lat1,lon2,lat2):
  radius = 3956 #miles
  x = math.pi/180.0
  a = (90.0-lat1)*(x)
  b = (90.0-lat2)*(x)
  theta = (lon2-lon1)*(x)
  c = math.acos((math.cos(a)*math.cos(b)) + (math.sin(a)*math.sin(b)*math.cos(theta)))
  return radius*c

这段Python代码的执行效率可以通过timeit来确定。

import timeit  
lon1, lat1, lon2, lat2 = -72.345, 34.323, -61.823, 54.826
num = 500000
t = timeit.Timer("p1.great_circle(%f,%f,%f,%f)" % (lon1,lat1,lon2,lat2),
           "import p1")
print "Pure python function", t.timeit(num), "sec"

执行50万次大概需要:2.2秒,太慢了。接下来尝试使用Cython进行改写,为了避免一下子代码变化太大,只使用Cython的类型声明“技能”,看看能达到什么效果。

import math
def great_circle(float lon1,float lat1,float lon2,float lat2):
  cdef float radius = 3956.0
  cdef float pi = 3.14159265
  cdef float x = pi/180.0
  cdef float a,b,theta,c
  a = (90.0-lat1)*(x)
  b = (90.0-lat2)*(x)
  theta = (lon2-lon1)*(x)
  c = math.acos((math.cos(a)*math.cos(b)) + (math.sin(a)*math.sin(b)*math.cos(theta)))
  return radius*c

通过给great_circle函数的参数、中间变量增加类型声明,Cython代码看起来跟原有的Python代码并无很大不同,业务逻辑代码一行没改。使用timeit的测定结果是大概1.8秒,提速将近二成,说明类型声明对性能提升非常有帮助。这时候,还有一个性能瓶颈需要解决,那就是:调用的math库是一个Python库,性能较差。解决这个问题,需要用到Cython的另一个技能:直接调用C函数。

cdef extern from "math.h":
  float cosf(float theta)
  float sinf(float theta)
  float acosf(float theta)
def great_circle(float lon1,float lat1,float lon2,float lat2):
  cdef float radius = 3956.0
  cdef float pi = 3.14159265
  cdef float x = pi/180.0
  cdef float a,b,theta,c
  a = (90.0-lat1)*(x)
  b = (90.0-lat2)*(x)
  theta = (lon2-lon1)*(x)
  c = acosf((cosf(a)*cosf(b)) + (sinf(a)*sinf(b)*cosf(theta)))
  return radius*c

Cython使用cdef extern from语法,将math.h这个C语言库头文件里声明的cofs、sinf、acosf等函数导入代码中。因为减少了Python函数调用和调用时产生的类型转换开销,使用timeit测定这个版本的代码的效率仅需要大概0.4秒的时间,性能提升了5倍有余。

通过这个例子,可以掌握Cython的两大技能:类型声明和直接调用C函数。只要再进一步参考Cython的文档,就可以尝试在项目中使用了。比起直接使用C/C++编写扩展模块,使用Cython的方法方便得多,成本也更低。

注意

除了使用Cython编写扩展模块提升性能之外,Cython也可用来把之前编写的C/C++代码封装成.so模块给Python调用(类似boost.python/SWIG的功能),Cython社区已经开发了许多自动化工具。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文