Skip to content

使用更快静态类型代码

原文: http://docs.cython.org/en/latest/src/quickstart/cythonize.html

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

使用其他类型声明,这可能如下所示:

# 经典风格
# integrate_cy.pyx
def f(double x):
    return x ** 2 - x

def integrate_f(double a, double b, int N):
    cdef int i
    cdef double s, dx
    s = 0
    dx = (b - a) / N
    for i in range(N):
        s += f(a + i * dx)
    return s * dx

# 纯Python风格
# 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 对象和从 Python 对象进行调用。在上面的示例中,假设参数在 f()内部和调用它时都是 C 中的double类型,但是float类型必须为参数构造一个 Python 对象才能传递它。

因此,Cython 提供了声明 C 风格函数的语法,即 cdef 关键字:

# 经典风格
cdef double f(double x) except? -2:
    return x ** 2 - x

# 纯Python风格
@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 子句。

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 Notebook的链接)会生成 Cython 代码和与其关联的C语言代码的 HTML 报告。线条根据“类型”的级别着色 - 白线转换为纯 C,而需要 Python C-API 的线条为黄色(因为它们转化为更多的 C-API 交互,因此更暗)。转换为 C 代码的行在前面有一个加号(+),可以单击以显示生成的代码。

当优化速度函数以及确定何时 释放 GIL时,此报告非常有用:通常,nogil 块可能只包含“白色”代码。

../../_img/htmlreport.png

请注意,Cython 根据其赋值(包括作为循环变量目标)推断出局部变量的类型,这也可以减少在任何地方显式指定类型的需要。例如,将dx声明为 double 类型是不必要的,就像在最后一个版本中声明s的类型一样(其中f的返回类型已知为 C 语言的double类型。)一个值得注意的例外然而,算术表达式中使用的是整数类型,因为 Cython 无法确保不会发生溢出(因此在需要 Python 的 bignums 时会回退到object)。要允许推断 C 整数类型,请将infer_types 编译器指令 设置为True。对于熟悉此语言功能的读者,此指令的工作类似于 C ++中的auto关键字。减少输入所有内容的需求可能会有很大帮助,但也可能导致意外,特别是不熟悉 c 类型的算术表达式的时候。这里有一个关于这些内容的简单概括。



回到顶部