cython介绍

使用cython加速慢如蜗牛的纯python代码。

What’s Cython

官方解释:Cython is Python with C data types,翻译过来就是Cython是可以使用C数据类型的python。在实际使用中,Cython可以让你在纯python基础上,使用C语言的数据类型定义python的数据,然后将python
代码转成C代码,再将其编译为动态链接库,在其他python程序中可以像其他库一样直接import编译的Cython模块。

使用Cython的的优点是:

  1. 能极大的加快程序的运行速度
  2. 能使用绝大多数python的第三方库
  3. 方便使用C函数和C库

但是为了使用它,需要修改原始代码,虽然修改量不算很大。

基本使用方法

安装Cython

Cython和python的其他第三发库一样,既可以使用pip等工具自动安装即可,也可以在官方下载安装文件手动安装。如果是使用的Anconda,那其中已经包含Cython了。

必要文件准备

Cython的使用和普通的第三方库可以直接在代码中import后就可以使用不一样,它是于pyinstaller、swig这种工具类似,除了需要在代码需要修改,还需要一些额外的操作。首先就是需要有两个python文件:

  1. 从原始的python代码修改的加入了C数据类型申明的“.pyx”文件,这就是我们需要加速的python代码文件
  2. setup.py文件,用于确定转换的一些配置选项,并启动Cython转换

.pyx文件的写法

.pyx文件由原始的.py文件而来,不过原始python代码也可以不做任何修改只把后缀名从“.py”变为“.pyx”,但是这样无法提高效率了。将.py文件改写成.pyx文件的主要操作就是把原有的python动态类型在使用前,先用C语言的数据类型加以声明,声明时使用“cdef”作为前缀关键字

如使用整型变量和整型数组时先声明如下:

1
2
cdef int n, i, len_p
cdef int p[1000]

在使用numpy的代码中,需要注意numpy的类型声明,下面为一个比较完整的函数改写,原始代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import numpy as np

def naive_dot(a, b):
if a.shape[1] != b.shape[0]:
raise ValueError('shape not matched')
n, p, m = a.shape[0], a.shape[1], b.shape[1]
c = np.zeros((n, m), dtype=np.float32)
for i in xrange(n):
for j in xrange(m):
s = 0
for k in xrange(p):
s += a[i, k] * b[k, j]
c[i, j] = s
return c

改写后为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import numpy as np
cimport numpy as np
cimport cython

@cython.boundscheck(False)
@cython.wraparound(False)
cdef np.ndarray[np.float32_t, ndim=2] _naive_dot(np.ndarray[np.float32_t, ndim=2] a, np.ndarray[np.float32_t, ndim=2] b):
cdef np.ndarray[np.float32_t, ndim=2] c
cdef int n, p, m
cdef np.float32_t s
if a.shape[1] != b.shape[0]:
raise ValueError('shape not matched')
n, p, m = a.shape[0], a.shape[1], b.shape[1]
c = np.zeros((n, m), dtype=np.float32)
for i in xrange(n):
for j in xrange(m):
s = 0
for k in xrange(p):
s += a[i, k] * b[k, j]
c[i, j] = s
return c

def naive_dot(a, b):
return _naive_dot(a, b)

上面要注意如下几点:

  1. 使用cimport导入numpy和cython
  2. 导入cython时为了调用其中的boundscheck和wraparound方法关闭边界检查,这个可选。
  3. 有返回值的函数定义没有了def,为了保证外部能使用,需要写个接口函数
  4. 函数的参数不需要“cdef”的前缀
  5. numpy数组的声明需要确定形状和元素类型

setup.py文件写法

最简单的setup.py文件内容如下(仅仅指定了需要转换的.pyx文件):

1
2
3
4
5
6
from distutils.core import setup
from Cython.Build import cythonize

setup(
ext_modules = cythonize("helloworld.pyx")
)

稍微复杂的(加了numpy使用的):

1
2
3
4
5
6
7
8
9
10
11
12
13
from distutils.core import setup, Extension
from Cython.Build import cythonize
import numpy
setup(ext_modules = cythonize(Extension(
'dot_cython',
sources=['dot_cython.pyx'],
language='c',
include_dirs=[numpy.get_include()],
library_dirs=[],
libraries=[],
extra_compile_args=[],
extra_link_args=[]
)))

具体写法需要参考官方文档。

转换流程

在命令行中执行如下命令,就可完成转化和编译:

1
python setup.py build_ext --inplace

完成后,会生成两个文件:.c文件和库文件(Windows后缀为.pyd,Linux后缀为.so)。在Windows中生成的库文件不要修改文件名,不然会无法使用。在使用时直接在python代码中欧个import库名就可以了。

在jupyter notebook中使用

在jupyter notebook中使用Cython会简单很多,因为jupyter notebook自动帮你执行的转化的操作,但是原始代码还是需要参照Cython的要求来修改。

使能Cython

在jupyter notebook中,使用单独的一个cell,写入:

1
%load_ext Cython

使用cython编译

在一个cell的最前面,写入:

1
%%cython

那个当前的cell中的代码就会使用cython来编译

使用不同cell中Cython数据对象

需要注意的是,在jupyter notebook中每个cell是独立的,在使用Cython的不同cell中,要用到其他Cython编译的cell中的数据对象,需要import:

1
from __main__ import xxx

注意点

  1. numpy数据引用,用[,]比[][]效率要高,普通的使用中对比不是特别明显,但是在cython编译循环语句中使用时差距就会变得非常大,甚至相差百倍: 对比二维数组赋值一个0.1s,一个0.002s
  2. 在cython中,有定义类型的数据和没有定义类型的数据做运算的时候,会严重拖慢速度完全起不到提速的效果,一定要对每个有参与计算的类型对象声明类型
  3. 在符合规范的情况下,cython编译的结果比纯python(不包含优化的第三方库)要快400多倍
  4. 一般n,m,k = img.shape 需要写成 n,m,k = img.shape[0], img.shape[1], img.shape[2] 才能编译成功
  5. numpy赋值时new_img = np.zeros((rgb_img.shape[0],rgb_img.shape[1],rgb_img.shape[2]), dtype=np.uint16)+256,拆成new_img = np.zeros((rgb_img.shape[0],rgb_img.shape[1],rgb_img.shape[2]),dtype=np.uint16)和new_img += 256两部分效率要高
  6. cython中使用numpy的数组的并行操作效率极差,要用for循环代替
Compartir