【问题标题】:What parts of a Numpy-heavy function can I accelerate with Cython我可以使用 Cython 加速 Numpy 重功能的哪些部分
【发布时间】:2022-01-23 12:16:57
【问题描述】:

介绍性说明:尝试使用 Cython 加速 Python+Numpy 代码是一个常见问题,这个问题试图创建一个关于可以有效加速哪些类型的操作的规范问题。虽然我试图用一个具体的例子来说明,但它只是一个例子——请不要过分关注这个毫无意义的例子。

另外,我已经为 Cython 做出了足够多的贡献,我应该声明一个隶属关系(鉴于我提出了这个话题)


实际问题

假设我有一个函数尝试对 Numpy 数组进行数值计算。它使用相当典型的操作:

  • 对不容易矢量化的数组元素进行循环
  • 调用 Numpy/Scipy 函数(在本例中为 np.sin)。
  • 对整个数组的数学运算 (a-b)
import numpy as np

def some_func(a, b):
    """
    a and b are 1D arrays

    This is intended to be illustrative! Please don't focus on what it
    actually does!
    """
    transformed_a = np.zeros_like(a)
    last = 0
    for n in range(1, a.shape[0]):
        an = a[n]
        if an > 0:
            delta = an - a[n-1]
            transformed_a[n] = delta*last
        else:
            last = np.sin(an)
    return transformed_a * b

a = np.random.randn(100)
b = np.linspace(0, 100, a.shape[0])

print(some_func(a, b))

我可以使用 Cython 加快速度吗?我希望哪些部分能够加快速度?

【问题讨论】:

  • 太棒了 - 很长一段时间以来,我们都需要这样的问答!
  • @ead 谢谢 - 我也很清楚!如果您认为我遗漏了任何重要的内容,请随时添加答案(或编辑我的答案)!

标签: python numpy cython


【解决方案1】:

索引单个数组元素

这是 Cython 可以真正帮助您的主要代码类型。在 Python 中索引单个元素(例如an = a[n])可能是一个相当慢的操作。部分原因是 Python 不是一种非常快的语言,因此在循环中多次运行 Python 代码可能会很慢,部分原因是该数组存储为 C 浮点数的紧凑数组,但索引操作需要返回一个 Python目的。因此,索引 Numpy 数组需要分配新的 Python 对象。

在 Cython 中,您可以将数组声明为 typed memoryviewsnp.ndarray。 (类型化的内存视图是更现代的方法,您通常应该更喜欢它们)。这样做允许您直接访问紧密封装的 C 数组并检索 C 值,而无需创建 Python 对象。

cython.boundscheckcython.wraparound 指令对于进一步加快索引速度非常值得(但请记住,它们确实删除了有用的功能,因此在使用它们之前三思)。

vs 向量化

很多时候,Numpy 数组上的循环可以写成向量化操作——一次作用于整个数组。像这样编写 Python+Numpy 代码通常是个好主意。如果您有多个链式矢量化操作,有时值得将其显式编写为 Cython 循环以避免分配中间数组。

另外,鲜为人知的 Cython Pythran backend 将一组矢量化 Numpy 操作转换为优化的 C++ 代码。

索引数组切片

在 Cython 中不是问题,但通常不会单独让您显着加速。

调用 Numpy 函数

例如last = np.sin(an)

这些需要 Python 调用,因此 Cython 通常无法加速这些 - 它无法查看 Numpy 函数的内容。

然而,这里的操作是针对单个值,而不是针对 Numpy 数组。在这种情况下,我们可以使用 C 标准库中的sin,这将比 Python 函数调用快得多。你会做from libc.math cimport sin 并调用sin 而不是np.sin

Numba 是一种替代 Python 加速器,它对 Numpy 函数具有更好的可见性,通常可以在不更改的情况下进行优化。

数组分配

例如transformed_a = np.zeros_like(a).

这只是一个 Numpy 函数调用,因此 Cython 无法加速它。如果它只是一个要返回给 Python 的中间值,那么您可以考虑在堆栈上放置一个固定大小的 C 数组

cdef double transformed_a[10]  # note - you must know the size at compile-time

或者通过 C 函数 malloc 分配它们(记住 free 它)。或者使用 Cython 的 cython.view.array(它仍然是一个 Python 对象,但可以更快一点)。

全数组算术

例如transformed_a * b,将 transformed_ab 逐个元素相乘。

Cython 在这里帮不了你——它只是一个伪装的函数调用(尽管 Pythran+Cython 可能有一些好处)。对于大型数组,这种操作在 Numpy 中非常有效,所以不要想太多。

请注意,没有为 Cython 类型的内存视图定义整个数组操作,因此您需要执行 np.asarray(memview) 才能将它们返回到 Numpy 数组。这通常不需要副本并且速度很快。

对于这样的一些操作,您可以使用BLASLAPACK 函数(它们是数组和矩阵操作的快速C 实现)。 Scipy 为他们 (https://docs.scipy.org/doc/scipy/reference/linalg.cython_blas.html) 提供了一个 Cython 界面供他们使用。与自然 Python 代码相比,它们使用起来稍微复杂一些。

说明性示例

为了完整起见,我会这样写:

import numpy as np
from libc.math cimport sin
cimport cython

@cython.boundscheck(False)
@cython.wraparound(False)
def some_func(double[::1] a, b):
    cdef double[::1] transformed_a = np.zeros_like(a)
    cdef double last = 0
    cdef double an, delta
    cdef Py_ssize_t n
    for n in range(1, a.shape[0]):
        an = a[n]
        if an > 0:
            delta = an - a[n-1]
            transformed_a[n] = delta*last
        else:
            last = sin(an)
    return np.asarray(transformed_a) * b

快了 10 倍多一点。

cython -a 在这里很有帮助 - 它会生成一个带注释的 HTML 文件,显示哪些行包含与 Python 的大量交互。

【讨论】:

  • 伟大的自我回答者!请注意,Cython 还可以帮助程序员编写并行 Numpy 代码。这一点很重要,因为现代桌面处理器大约有 4-8 个内核,而计算服务器处理器通常有 16-32 个内核。更不用说未来的核心数量预计还会增长。
  • 谢谢。为简洁起见,我省略了并行代码,但我同意它有时非常有用
  • 除了 prange,我建议提到多个循环作为“值得写为 numpy 矢量化表达式;如果做不到,请使用 cython”。这可能是索引单个元素部分的子集。
  • 这太棒了!我的一个问题是关于使用mallocPyMem_Malloc 之间的区别。此外,如果有使用这些的一般准则,例如tryfinally(假设在 cython 函数内部创建的所有动态数组在返回之前都被删除)。另一件事是您将int 用于n,但是为什么/何时使用long 或者更好的是Py_ssize_t 来代替。讨论了最后一个问题here
  • @Breno Py_ssize_t 肯定会比 int 更好。我会更新的。我对mallocPyMem_Malloc - as far as I can see they're usually the same 没有强烈的看法,但只要确保free 匹配即可。如果生命周期在单个函数中,try/finally 是确保释放内存的好主意。否则,您通常希望 Python 对象在析构函数中释放内存。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-08-20
  • 1970-01-01
  • 2020-05-02
  • 2015-07-28
  • 1970-01-01
  • 2013-08-29
  • 1970-01-01
相关资源
最近更新 更多