众所周知,Python是一门“极慢”的语言,无奈它写起来真的很快,各种库用起来太顺手了,以至于形成了良性的生态循环。这也导致越来越多的人用起了Python(主要是有很多代码可以抄啊…)。

但是在某些领域,对代码运行效率的要求还是挺高的,比如本人所在的优化算法领域。那我又想拥有Python的快速开发能力,又想获得较高的运行时效率,这怎么办?

有很多的加速方案,比如pybind11、numba、Cython等,个人一番体验下来觉得最简单的就是Cython啦,绝对是可以以最低成本获得不错效果的加速手段(之一?)。

这篇文章就简单介绍下Cython的一些核心用法,我也录了相关的系列视频在B站,建议关注我的B站Cython系列,其中包含完整的代码示例。

首先,Cython是什么?Cython 是一个功能强大的工具,它允许你以 Python 语言为基础编写代码,并通过一些优化技巧将其转化为 C 代码,从而提高运行时效率。文章标题只有浅出,没有深入,因为我自以为并没有很深入了解Cython,只是希望介绍一些 Cython 的核心用法,帮大家以最低成本(相对小的代码修改)获得不错的加速效果,具体包括指定变量类型、边界检查优化、内存视图优化以及 cdef、def 和 cpdef 的区别。Cython还有一些高级用法,比如直接给C/C++代码加wrapper,个人认为已经偏离Python太远了,非必要就不研究了。

基础配置

当你开始使用 Cython 进行项目加速时,需要了解一些基础配置和工作流程。在本部分,我们将介绍 Cython 的安装,创建 setup.py 文件以及将 Cython的.pyx 文件转换为 .c 并编译为扩展模块的过程。这部分可参考之前的文章Accelerate Your NumPy Matrix Operations with Cython

安装 Cython

首先,你需要安装 Cython。你可以使用 pip 来安装 Cython:

1
pip install Cython

创建 pyx 文件

Cython的模块需要以.pyx为后缀,所以我们需要创建一个.pyx文件,比如multiply.pyx,内容如下,这个代码可以直接把Python的代码复制过来(没错,一个字都不改也ok,当然这样加速效果有限):

1
2
3
4
5
6
7
8
9
10
11
12
13
def cnp.ndarray[double, ndim=2] elementwise_multiply(
cnp.ndarray[double, ndim=2] A,
cnp.ndarray[double, ndim=2] B):
cdef int nrows = A.shape[0]
cdef int ncols = A.shape[1]
cdef cnp.ndarray[double, ndim=2] result = np.zeros((nrows, ncols), dtype=np.float64)

cdef int i, j
for i in range(nrows):
for j in range(ncols):
result[i, j] = A[i, j] * B[i, j]

return result

创建 setup.py 文件

setup.py 文件是用于构建和安装你的 Cython 扩展模块的关键文件。一般放在根目录,便于对整个项目进行管理,以下是一个简单的 setup.py 示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from setuptools import setup, Extension
from Cython.Build import Cythonize
import numpy as np

ext_modules = [
Extension("acceleration.multiply",
["acceleration/Cython/multiply.pyx"],
include_dirs=[np.get_include()]),
Extension("acceleration.knapsack",
["acceleration/Cython/knapsack.pyx"],
include_dirs=[np.get_include()])
]

setup(
name='Fluent Python',
ext_modules=Cythonize(ext_modules),
)

在这个示例中,我添加了两个Cython模块,multiplyknapsack。写法上,Extension的第一个参数是编译后扩展模块的路径,第二个参数是.pyx文件的路径,第三个参数是需要包含的头文件路径,这里我用了numpy的头文件。

setup函数的name参数是项目名称,ext_modules参数是需要编译的模块,这里用了Cythonize函数。

编译和安装

你可以通过 setup.py 文件来编译和安装你的扩展模块。在项目根目录下运行以下命令:

1
Python setup.py build_ext --inplace

这将编译扩展模块并将其安装到指定目录下,而生成的.c文件在相应的.pyx同级目录下。

扩展模块在windows下是.pyd文件,在linux下是.so文件,比方我用的是windows,所以生成的文件是类似于multiply.cp39-win_amd64.pydknapsack.cp39-win_amd64.pyd

使用 Cython 扩展模块

现在,你可以在 Python 中正常导入和使用你的 Cython 扩展模块(把.pyd.so文件当成是一个Python的模块就行)。例如,如果你的扩展模块名称是 your_module,可以这样导入它:

1
import your_module

然后,你可以使用其中定义的函数和类,它们已经被编译成 C 代码,因此在性能上比纯 Python 代码更高效。

Cython 的核心用法

在上一部分,我们介绍了 Cython 的基础配置和工作流程。在本部分,我们将介绍 Cython 的一些核心用法,包括指定变量类型、边界检查优化、内存视图优化以及 cdef、def 和 cpdef 的区别。

指定变量类型

Python 是一门动态类型语言,这意味着变量的类型在运行时确定。这种灵活性使得代码编写和调试变得非常容易,但也导致了运行效率较低。Cython 允许你为变量和函数参数指定静态类型,这样编译器就能更好地优化你的代码。

例如,考虑以下 Python 函数:

1
2
def add(a, b):
return a + b

在 Cython 中,你可以使用类型声明来指定参数的类型:

1
2
def add(int a, int b):
return a + b

这样,Cython 编译器可以生成更高效的 C 代码,无需在运行时进行类型检查。

这里列举一下Python和Cython的类型对应关系如下[1]:

当然除了这些基础类型外,Cython还支持从C++标准库中导入一些类型,或者导入numpy的数组类型,可以看看我的B站视频

边界检查优化

Python 的列表和数组通常会在访问元素时执行边界检查,以确保不会访问超出范围的元素。这会导致额外的性能开销。在 Cython 中,你可以使用数组的 “boundscheck” 属性和 “wraparound” 属性来控制边界检查。

1
2
3
4
5
6
7
# 在Cython中关闭边界检查
cimport Cython

@Cython.boundscheck(False)
@Cython.wraparound(False)
def access_element(my_list):
return my_list[10]

通过关闭边界检查,你可以显著提高代码的执行速度,但需要手动确保访问的索引不会超出数组的实际范围。

内存视图优化

Cython 提供了内存视图(memoryview)的支持,它允许你有效地访问和操作数组的内存。内存视图可以加速数组的操作,尤其是多维数组的索引及切片操作。

1
2
3
4
5
cimport Cython

def process_array(double[:] arr):
for i in range(arr.shape[0]):
arr[i] = arr[i] * 2
  • double[:] arr 定义了一个内存视图,该视图可以访问以 arr 为基础的双精度浮点数数组。[:] 表示可以访问整个数组。
  • arr.shape[0] 返回数组的长度,允许你在循环中迭代整个数组。
  • 在循环中,你可以直接访问内存视图 arr 的元素,并执行操作,而不需要生成临时对象。

一般来说,Python的切片操作是通过生成一个slice对象,再把该对象传入一个函数来实现的,内存视图允许你以一种更接近底层的方式操作数据,内存视图的优点在于它们不会复制数据,而是在原始数据的基础上执行操作。这减少了内存使用量和数据复制开销,因此在处理大型数据集时,内存视图可以显著提高性能。

cdef、def 和 cpdef 的区别

在 Cython 中,有三种不同的函数声明方式:cdef、def 和 cpdef。

  • cdef 声明的函数是纯 C 函数,只能从 Cython 代码中调用,不可从 Python 代码中访问。
  • def 声明的函数是 Python 函数,可以从 Python 代码中调用,但会带来一些性能开销。
  • cpdef 声明的函数是混合函数,既可以从 Cython 代码中调用,也可以从 Python 代码中调用,其底层其实生成了两个函数版本。

选择适当的声明方式取决于你的需求,如果需要最大的性能,可以使用 cdef 声明,如果需要与 Python 交互,可以使用 cpdef

以上是 Cython 的一些核心用法,它们可以帮助你在不牺牲开发速度的情况下提高 Python 代码的运行效率。上述示例可能并不完整,如果你对 Cython 感兴趣,可以关注我的Cython系列视频,其中包括更多实际案例和示范。

其他资料

[1] Welcome to Cython’s Documentation — Cython 3.0.2 documentation

[2] PyVideo.org https://pyvideo.org/search.html?q=Cython
Cython [Book]

[3] Cython: The Best of Both Worlds

[4] Proceedings of the Python in Science Conference (SciPy): Cython tutorial

[5] Cython 3.0: Compiling Python to C, the next generation - YouTube

Ref

[1] Language Basics — Cython 3.0.2 documentation