通过静态类型实现更快的代码

注意

此页面使用两种不同的语法变体

  • 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 代码

integrate.py
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%。这比没有好,但添加一些静态类型可以产生更大的差异。

通过添加类型声明,这可能看起来像

integrate_cy.py
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

由于迭代器变量 i 使用 C 语义进行类型化,因此 for 循环将被编译为纯 C 代码。对 asdx 进行类型化很重要,因为它们参与了 for 循环中的算术运算;对 bN 进行类型化影响较小,但在这种情况下,保持一致并对整个函数进行类型化并不需要太多额外工作。

这导致速度比纯 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

通常应该添加某种形式的 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 块只能包含“白色”代码。

../../_images/htmlreport_py.png

请注意,Cython 会根据局部变量的赋值(包括作为循环变量的目标)推断其类型,这也可以减少在所有地方显式指定类型的需要。例如,在上面声明 dx 为 double 类型是多余的,就像在最后一个版本中声明 s 的类型一样(因为 f 的返回值已知为 C double)。但是,一个值得注意的例外是 *用于算术表达式的整数类型*,因为 Cython 无法确保不会发生溢出(因此在需要 Python 的大数时会回退到 object)。为了允许推断 C 整数类型,请将 infer_types 指令 设置为 True。此指令的作用类似于 C++ 中的 auto 关键字,对于熟悉此语言特性的读者来说。它可以极大地帮助减少键入所有内容的需要,但它也可能导致意外情况。特别是如果一个人不熟悉使用 c 类型的算术表达式。可以在这里找到这些表达式的快速概述 这里