通过静态类型实现更快的代码¶
注意
此页面使用两种不同的语法变体
Cython 特定的
cdef
语法,旨在使类型声明简洁,并易于从 C/C++ 的角度读取。纯 Python 语法,允许在 纯 Python 代码 中进行静态 Cython 类型声明,遵循 PEP-484 类型提示和 PEP 526 变量注释。
要在 Python 语法中使用 C 数据类型,您需要在要编译的 Python 模块中导入特殊的
cython
模块,例如import cython
如果您使用纯 Python 语法,我们强烈建议您使用最新的 Cython 3 版本,因为与 0.29.x 版本相比,这里已经进行了重大改进。
Cython 是一个 Python 编译器。这意味着它可以编译正常的 Python 代码,无需更改(除了某些尚不支持的语言特性,请参见 Cython 限制)。但是,对于性能关键的代码,添加静态类型声明通常很有帮助,因为它们将允许 Cython 退出 Python 代码的动态特性,并生成更简单、更快的 C 代码 - 有时速度会提高几个数量级。
但是,必须注意,类型声明可能会使源代码更冗长,从而降低可读性。因此,不建议在没有充分理由的情况下使用它们,例如,当基准测试证明它们确实在性能关键部分使代码速度明显提高时。通常,在正确的位置使用几个类型就能起到很大的作用。
所有 C 类型都可用于类型声明:整数和浮点数类型、复数、结构体、联合体和指针类型。Cython 可以自动并在赋值时正确地进行类型转换。这也包括 Python 的任意大小整数类型,其中将值转换为 C 类型时发生的溢出将在运行时引发 Python OverflowError
。(但是,它不会在进行算术运算时检查溢出。)生成的 C 代码将在此情况下正确安全地处理 C 类型的平台相关大小。
类型通过 cdef 关键字声明。
类型化变量¶
考虑以下纯 Python 代码
def f(x):
return x ** 2 - x
def integrate_f(a, b, N):
s = 0
dx = (b - a) / N
for i in range(N):
s += f(a + i * dx)
return s * dx
仅在 Cython 中编译此代码只会使速度提高 35%。这比没有好,但添加一些静态类型可以产生更大的差异。
通过添加类型声明,这可能看起来像
def f(x: cython.double):
return x ** 2 - x
def integrate_f(a: cython.double, b: cython.double, N: cython.int):
i: cython.int
s: cython.double
dx: cython.double
s = 0
dx = (b - a) / N
for i in range(N):
s += f(a + i * dx)
return s * dx
def f(double x):
return x ** 2 - x
def integrate_f(double a, double b, int N):
cdef int i
cdef double s
cdef double dx
s = 0
dx = (b - a) / N
for i in range(N):
s += f(a + i * dx)
return s * dx
由于迭代器变量 i
使用 C 语义进行类型化,因此 for 循环将被编译为纯 C 代码。对 a
、s
和 dx
进行类型化很重要,因为它们参与了 for 循环中的算术运算;对 b
和 N
进行类型化影响较小,但在这种情况下,保持一致并对整个函数进行类型化并不需要太多额外工作。
这导致速度比纯 Python 版本快 4 倍。
类型化函数¶
Python 函数调用可能很昂贵 - 在 Cython 中更是如此,因为可能需要进行 Python 对象的转换才能进行调用。在上面的示例中,参数被假定为 f()
内部和对它的调用中都是 C double,但必须围绕参数构建 Python float
对象才能传递它。
因此,Cython 提供了一种声明 C 风格函数的方法,即 Cython 特定的 cdef
语句,以及 @cfunc
装饰器,用于在 Python 语法中声明 C 风格函数。这两种方法是等效的,并生成相同的 C 代码
@cython.cfunc
@cython.exceptval(-2, check=True)
def f(x: cython.double) -> cython.double:
return x ** 2 - x
cdef double f(double x) except? -2:
return x ** 2 - x
通常应该添加某种形式的 except 修饰符,否则 Cython 将无法传播在函数(或其调用的函数)中引发的异常。 except? -2
表示如果返回 -2
,则会检查错误(尽管 ?
表示 -2
也可能用作有效返回值)。可以使用仅包含 Python 语法的装饰器 @exceptval(-2, check=True)
来表达相同的意思。
或者,较慢的 except *
始终是安全的。如果函数返回 Python 对象,或者保证在函数调用中不会引发异常,则可以省略 except 子句。同样,Cython 提供了装饰器 @exceptval(check=True)
,提供相同的功能。
cdef
(和 @cfunc
装饰器)的副作用是该函数不再从 Python 空间可见,因为 Python 不知道如何调用它。在运行时也无法更改 f()
。
使用 cpdef
关键字而不是 cdef
,还会创建一个 Python 包装器,以便该函数既可从 Cython(快速,直接传递类型化值)访问,也可从 Python(将值包装在 Python 对象中)访问。实际上, cpdef
不仅提供 Python 包装器,它还安装逻辑以允许该方法被 python 方法覆盖,即使是从 cython 内部调用也是如此。与 cdef
方法相比,这确实会增加一点点开销。同样,Cython 提供了一个 @ccall
装饰器,它提供与 cpdef
关键字相同的功能。
加速:比纯 Python 快 150 倍。
确定在何处添加类型¶
由于静态类型通常是获得大幅度速度提升的关键,因此初学者往往倾向于对所有内容进行类型化。这会降低可读性和灵活性,甚至会降低速度(例如,通过添加不必要的类型检查、转换或缓慢的缓冲区解包)。另一方面,忘记对关键循环变量进行类型化很容易降低性能。两个帮助完成此任务的基本工具是分析和注释。分析应该是任何优化工作的第一步,它可以告诉你你在哪里花费了时间。然后,Cython 的注释可以告诉你你的代码为什么需要时间。
对 cython
命令行程序使用 -a
开关(或从 Sage 笔记本中遵循链接)会导致生成一个 HTML 报告,其中包含 Cython 代码与生成的 C 代码交织在一起。行根据“类型化”程度进行颜色编码 - 白色行转换为纯 C,而需要 Python C-API 的行则为黄色(更深表示它们转换为更多 C-API 交互)。转换为 C 代码的行前面有一个加号 (+
),可以单击以显示生成的代码。
当优化函数以提高速度时,此报告非常宝贵,并且可以确定何时可以 释放 GIL(请注意,释放 GIL 仅在有限的情况下有用,有关更多详细信息,请参阅 Cython 和 GIL):通常, nogil
块只能包含“白色”代码。
请注意,Cython 会根据局部变量的赋值(包括作为循环变量的目标)推断其类型,这也可以减少在所有地方显式指定类型的需要。例如,在上面声明 dx
为 double 类型是多余的,就像在最后一个版本中声明 s
的类型一样(因为 f
的返回值已知为 C double)。但是,一个值得注意的例外是 *用于算术表达式的整数类型*,因为 Cython 无法确保不会发生溢出(因此在需要 Python 的大数时会回退到 object
)。为了允许推断 C 整数类型,请将 infer_types
指令 设置为 True
。此指令的作用类似于 C++ 中的 auto
关键字,对于熟悉此语言特性的读者来说。它可以极大地帮助减少键入所有内容的需要,但它也可能导致意外情况。特别是如果一个人不熟悉使用 c 类型的算术表达式。可以在这里找到这些表达式的快速概述 这里。