第三章 深入Cython

深入Cython

本章主要关注为什么Cython能如此好的加速Python代码,归因于两个不同:

  • 1.运行时解释和提前编译
  • 2.动态和静态类型

解释和编译后执行

字节码和机器码的桥梁:Python解释器可以直接运行编译后的C代码,并且对最终用户透明。这些C代码必须编译成特殊类型的动态库,作为扩展模块,这些模块是完整的Python模块,当运行这部分扩展模块的时候,Python虚拟机不在解释字节码,而是直接运行编译后的机器码,这将节省Python解释器的时间开销。
这种方式大概能提升Python 10%~30%的性能,但是真正性能改进来自用静态类型替代Python的动态类型。

动态类型和静态类型

静态类型通过指定变量类型,编译器将基础的操作进行优化,而动态类型获得了灵活性却将大量的时间浪费在了变量类型推断和基础操作中。

静态类型定义关键字:cdef

使用C语言的变量类型,就要按照C语言的构造规则,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
cdef int i, j, k
cdef float price, margin
cdef int i = 0
cdef long int j = 0, k = 0
cdef float price = 0.0, margin = 1.0
def integrate(a, b, f):
cdef int i
cdef int N=2000
cdef float dx, s=0.0
dx = (b-a)/N
for i in range(N):
s += f(a+i*dx)
return s * dx
#也可以使用cdef代码块
def integrate(a, b, f):
cdef:
int i
int N=2000
float dx, s=0.0
#...

cdef声明对应表:
cdef声明对应表

Cython中的自动类型推断

使用cdef不是Cython的唯一的定义静态变量的方法,Cython还支持自动推断未被定义类型的函数和函数体,默认情况下,Cython只有在不会改变代码语义的情况下才会使用自动类型推断。
可以使用infer_types编译指令来给Cython的自动推断更好的控制,infer_types编译指令可以用在函数作用域或者全局作用域,如下:

1
2
3
4
5
6
7
8
9
cimport cython
@cython.infer_types(True)
def more_inference():
i = 1
d = 2.0
c = 3+4j
r = i * d + c
return r

当时用infer_types编译指令的时候,我们应该负责人的确定整数不会溢出,代码语义不会改变,使用它可以很轻易的测试是否改变了代码的结果,以及是否有不同的性能。

Cython中的C指针

在Cython中,我们可以使用C的语法和语义定义指针,如下:

1
2
3
cdef int *p_int
cdef float** pp_float = NULL
cdef int *a, *b

由于Python中有和*语法,所以Cython中指针的引用跟C语言中的语法有些不同,有下面两种引用方式:

1
2
3
4
5
6
7
cdef double golden_ratio
cdef double *p_double
p_double = &golden_ratio
#下面方式不常用
from cython cimport operator
print operator.dereference(p_double)

另外,Cython中指针指向结构体和C语言中也不同:

1
2
3
4
5
6
7
#C语言用法:
st_t *p_st = make_struct();
int a_doubled = p_st->a + p_st->a;
#Cython用法:
cdef st_t *p_st = make_struct()
cdef int a_doubled = p_st.a + p_st.a

混合静态和动态类型变量

Cython允许静态类型和动态类型变量之间的赋值,如下面例子:

1
2
3
4
#今天的int类型
cdef int a, b, c
#动态元祖类型
tuple_of_ints = (a, b, c)

这个简单的例子能运行是因为C语言中的int和Python中的int有明显的对应关系,如果a,b,c变量是指针,那么这个例子就不能工作,需要先将指针的应用内容放入元祖中或者采用其他策略。
下面是C/C++和Python的类型对应关系表:
C/C++和Python的类型对应关系表

int类型转换和溢出

注意:int类型的转换需要考虑整数溢出的问题,如果C类型不能代表Python的整数,运行时将会抛出OverflowError异常,我们可以设置overflowcheck和overflowcheck.fold编译指令来捕捉溢出错误

str和unicode类型

str或unicode的类型转换到char *或std::string类型需要设置c_string_type和c_string_encoding编译指令。

用Python类型声明静态变量

目前为止,我们已经使用cdef关键字声明C类型的静态变量,他也能够声明Python类型的静态变量,我们可以使用内置的变量类型,如list,tuple,dict,扩展的类型,如NumPy的arrays类型,和其他的许多类型。
但并不是所有的Python类型都能被静态声明:他们必须用C语言实现,而且Cython能访问他们的声明。Python内置的类型已经满足这些要求,静态声明他们很简单,如下:

1
2
3
4
cdef list particles, modified_particles
cdef dict names_from_particles
cdef str pname
cdef set unique_particles

在底层,Cython使用C指针指向他们Python内置的结构体类型声明,他们能像普通的Python类型一样使用,但是仅限于他们声明的类型。
注意:C语言和Python的除法和取模语义不一样,会有不同的计算行为。默认情况下Cython使用Python的语法语义来计算除法和取模,当然可以使用cdivision编译指令来改变这一行为。

静态类型对加速的意义

Cython的一般原理:我们提供的静态信息类型越多,Cython优化的结果就越好。
Cython目前支持下面一些Python内置的类型静态声明:

  • type, object
  • bool
  • complex
  • basestring, str, unicode, bytes, bytearray
  • list, tuple, dict, set, frozenset
  • array
  • slice
  • date, time, datetime, timedelta, tzinfo
    更多的类型支持可能会在未来的版本实现。
    Python有一个PyLongObjects的C级别的对象来表示任意大小的整数,Cython提供了一个适当的语言无关的方法转换C的整数和Python的整数类型,在转换过程中不可能抛出OverflowError异常。

引用计数和静态字符串类型

1
2
3
4
5
6
7
8
9
10
11
#这种赋值方式会报错
b1 = b"All men are mortal."
b2 = b"Socrates is a man."
cdef char *buf = b1 + b2
#下面两种才行
tmp = s1 + s2
cdef char *buf = tmp
cdef bytes tmp = s1 + s2
cdef char *buf = tmp

Cython的三种类型函数

Python的函数比C语言的函数要灵活强大的多,但是代价是比C语言慢很多,Cython支持同一个文件内C语言的函数和Python的函数相互调用。

Cython中的Python函数用def关键字

Cython中支持使用def关键字定义Python类型的函数,而且跟在Python的使用一样

Cython中的C类型函数使用cdef关键字

使用cdef关键字定义C语言类型的函数,参数和返回值都是静态类型,可以使用指针,结构体和其他C语言类型,而不会自动转到Python类型,但是却是Python语法风格如下:

1
2
3
4
5
cdef long c_fact(long n):
"""Computes n!"""
if n <= 1:
return 1
return n * c_fact(n - 1)

cdef函数在同一个源文件中可以被任何def和cdef函数调用,但是cdef函数不允许被外部的Python代码调用,由于这个限制,cdef函数通常用来作为快速辅助来帮助def函数完成任务。
如果我们想要在外部的Python代码使用cdef函数,我们需要用def函数包装cdef函数,如下:

1
2
3
def wrap_c_fact(n):
"""Computes n!"""
return c_fact(n)

使用cpdef结合def和cdef函数

cpdef函数结合了def和def函数的特点,解决了他们的很多局限性,cpdef结合了def的可访问性和cdef的高效性,如下:

1
2
3
4
5
cpdef long cp_fact(long n):
"""Computes n!"""
if n <= 1:
return 1
return n * cp_fact(n - 1)

cpdef有一个限制,由于cpdef肩负着Python和C的双重责任,他的参数和返回值类型必须是Python和C的兼容类型。任何Python类型都可以在C级别被表示,但是并不是所有的C类型都可以在Python中被表示,所以我们不能在cpdef函数中使用void、C指针或者C数组等类型作为返回值或者参数。

函数和异常处理

def函数返回的是Python类型,如果出现异常,异常能被正确的传播出去,但是cdef和cpdef函数返回的不是Python类型,出现异常会产生警告,异常会被忽略,不会被传播出去,为了传播异常,Cython提供了except子句,允许cdef和cpdef函数产生异常时和他的调用者进行交流。

1
2
cpdef int divide_ints(int i, int j) except? -1:
return i / j

如果发生异常,函数会返回-1作为检查异常的哨兵,我们不用手动返回-1,Cython会自动处理这个值,这个值是任意的,可以设置成任何整数值.

函数和embedsignature编译指令

embedsignature编译指令设置为True能帮助生成默认的签名,知道参数名和函数的默认值。

强制类型转换

Cython中的类型转换规则跟C语言的类型转换规则很像。

声明和使用structs、unions和enums

Cython也支持声明、创建和使用C语言的struct、union和enum类型。例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#C语言中声明struct和union
struct mycpx {
int a;
float b;
};
union uu {
int a;
short b, c;
};
#Cython中声明struct和union
cdef struct mycpx:
float real
float imag
cdef union uu:
int a
short b, c

也可以使用它ctypedef,定义另一种struct或者union的类型别名,然后使用cdef定义使用 ,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ctypedef struct mycpx:
float real
float imag
ctypedef union uu:
int a
short b, c
#几种使用方式
cdef mycpx zz
zz.real = 3.1415
zz.imag = -1.0
cdef mycpx a = mycpx(3.1415, -1.0)
cdef mycpx b = mycpx(real=2.718, imag=1.618034)
cdef mycpx zz = {'real': 3.1415, 'imag': -1.0}

嵌套struct的使用

1
2
3
4
5
6
7
8
cdef struct _inner:
int inner_a
cdef struct nested:
int outer_a
_inner inner
cdef nested n = {'outer_a': 1, 'inner': {'inner_a': 2}}

enum也可以使用ctypedef和cdef关键字声明使用,和struct、union类似,实例:

1
2
3
4
5
6
7
8
9
10
11
cdef enum PRIMARIES:
RED = 1
YELLOW = 3
BLUE = 5
cdef enum SECONDARIES:
ORANGE, GREEN, PURPLE
#匿名enum在定义全局整数常数时很有用
cdef enum:
GLOBAL_SEED = 37

使用ctypedef声明类型别名

Cython支持ctypedef关键字,他的用法和C语言的typedef声明类似,在和外部代码对接时很有用,下面是一个简单的例子:

1
2
3
4
5
6
7
ctypedef double real
ctypedef long integral
def displacement(real d0, real v0, real a, real t):
"""Calculates displacement under constant acceleration."""
cdef real d = d0 + (v0 * t) + (0.5 * a * t**2)
return d

一个ctypedef声明必须在文件域内,不能再函数域声明。

Cython的for循环和while循环

Cython支持无改动的Python的for循环和while循环的优化,它能够自推断类型并生成优化代码,但是并不是任何时候都会这样做,比如下面代码,n是动态的类型,Cython将不会进行C语言的加速优化,将n改为静态类型才会进行加速优化

1
2
3
4
5
6
7
8
n = 100
for i in range(n):
# ...
#加速优化
cdef unsigned int i, n = 100
for i in range(n):
# ...

高效for循环指南

将循环变量声明为静态类型,Cython会加速优化for循环,但是如果你在循环中对列表进行索引,Cython不会检查索引是否越界。
当容器是list, tuple, dict等等类型时,静态的循环索引变量也会带来很大的开销,为了for循环更加高效,可以使用其他的更加高效的容器替换。

1
2
3
4
5
6
7
8
n = len(a) - 1
for i in range(1, n):
a[i] = (a[i-1] + a[i] + a[i+1]) / 3.0
#优化后
cdef unsigned int i, n = len(a) - 1
for i in range(1, n):
a[i] = (a[i-1] + a[i] + a[i+1]) / 3.0

Cython预编译

Cython有一个DEF关键字创建宏,是一个编译时符号,类似于C语言的#define,DEF常数必须在编译时被解析,而且只能是简单类型,它们可以是浮点数、整数或者字符串,如下:

1
2
3
4
5
6
DEF E = 2.718281828459045
DEF PI = 3.141592653589793
def feynmans_jewel():
"""Returns e**(i*pi) + 1. Should be ~0.0"""
return E ** (1j * PI) + 1.0

连接Python2和Python3的桥梁

我们可以用Python2或者Python3语法编写Cython的.pyx文件,生成的C语言代码是兼容Python2和Python3的,也就是说任何Cython代码都能编译成Python2模块或者Python3模块。Cython会自动推断Python版本,也可以使用参数来指定编译版本:

1
2
3
4
#指定以Python3版本编译
$ cython -3 file.pyx
#指定以Python2版本编译
$ cython -2 file.pyx

文章目录
  1. 1. 解释和编译后执行
    1. 1.1. 动态类型和静态类型
    2. 1.2. 静态类型定义关键字:cdef
    3. 1.3. Cython中的自动类型推断
    4. 1.4. Cython中的C指针
    5. 1.5. 混合静态和动态类型变量
    6. 1.6. int类型转换和溢出
    7. 1.7. str和unicode类型
    8. 1.8. 用Python类型声明静态变量
    9. 1.9. 静态类型对加速的意义
    10. 1.10. 引用计数和静态字符串类型
  2. 2. Cython的三种类型函数
    1. 2.1. Cython中的Python函数用def关键字
    2. 2.2. Cython中的C类型函数使用cdef关键字
    3. 2.3. 使用cpdef结合def和cdef函数
  3. 3. 函数和异常处理
  4. 4. 函数和embedsignature编译指令
  5. 5. 强制类型转换
  6. 6. 声明和使用structs、unions和enums
  7. 7. 使用ctypedef声明类型别名
  8. 8. Cython的for循环和while循环
    1. 8.1. 高效for循环指南
  9. 9. Cython预编译
  10. 10. 连接Python2和Python3的桥梁
|