Cython浅出:最常用知识点都在这儿了
众所周知,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 | def cnp.ndarray[double, ndim=2] elementwise_multiply( |
创建 setup.py 文件
setup.py 文件是用于构建和安装你的 Cython 扩展模块的关键文件。一般放在根目录,便于对整个项目进行管理,以下是一个简单的 setup.py 示例:
1 | from setuptools import setup, Extension |
在这个示例中,我添加了两个Cython模块,multiply和knapsack。写法上,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.pyd和knapsack.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 | def add(a, b): |
在 Cython 中,你可以使用类型声明来指定参数的类型:
1 | def add(int a, int b): |
这样,Cython 编译器可以生成更高效的 C 代码,无需在运行时进行类型检查。
这里列举一下Python和Cython的类型对应关系如下[1]:
当然除了这些基础类型外,Cython还支持从C++标准库中导入一些类型,或者导入numpy的数组类型,可以看看我的B站视频。
边界检查优化
Python 的列表和数组通常会在访问元素时执行边界检查,以确保不会访问超出范围的元素。这会导致额外的性能开销。在 Cython 中,你可以使用数组的 “boundscheck” 属性和 “wraparound” 属性来控制边界检查。
1 | # 在Cython中关闭边界检查 |
通过关闭边界检查,你可以显著提高代码的执行速度,但需要手动确保访问的索引不会超出数组的实际范围。
内存视图优化
Cython 提供了内存视图(memoryview)的支持,它允许你有效地访问和操作数组的内存。内存视图可以加速数组的操作,尤其是多维数组的索引及切片操作。
1 | cimport Cython |
- 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
