我严重破坏了 Cython,它的性能比纯 Python 还差。为什么?

I've mangled Cython badly, it's performing worse than pure Python. Why?

我是 Python 的新手,完全不了解 运行 C(不幸的是),所以我很难正确理解使用 Cython 的某些方面。

在分析了一个 Python 程序并发现它只是几个循环占用大部分时间后,我决定考虑将它们转储到 Cython 中。最初,我只是让 Cython 按原样解释 Python,结果是(非常好!)~2 倍的速度提升。酷!

从 Python main 开始,我向函数传递了两个二维数组("a" 和 "b")和一个浮点数 "d",它 returns 一个列表,"newlist"。例如:

a =[[12.7, 13.5, 1.0],[23.4, 43.1, 1.0],...]
b =[[0.46,0.95,0],[4.56,0.92,0],...]
d = 0.1

这是原始代码,只是为 Cython 添加了 cdef:

def loop(a, b, d):

    cdef int i, j
    cdef double x, y

    newlist = []

    for i in range(len(a)):
        if b[i][2] != 1:
            for j in range(i+1,len(a)):
                if a[i] == a[j] and b[j][2] != 1:
                    x = b[i][0]+b[j][0]
                    y = b[i][1]+b[j][1]
                    b[i][2],b[j][2] = 1,1

                    if abs(y)/abs(x) > d:
                        if y > 0: newlist.append([a[i][0],a[i][1],y])

    return newlist

在 "pure Python" 中,这个 运行(有几万个循环)在 ~12.5 秒内。在 Cython 中,它在 ~6.3 秒内 运行。已完成接近零的工作,取得了巨大进步!

然而,通过一些阅读,很明显可以做更多的事情,所以我开始尝试应用一些类型更改来让事情进展得更快,遵循 Cython 文档,here(也在评论中引用)。

以下是收集的修改,旨在模仿 Cython 文档:

import numpy as np
cimport numpy as np

DtypeA = np.float
DtypeB = np.int

ctypedef np.float_t DtypeA_t
ctypedef np.int_t DtypeB_t

def loop(np.ndarray[DtypeA_t, ndim=2] A,
         np.ndarray[DtypeA_t, ndim=2] B,
         np.ndarray[DtypeB_t, ndim=1] C,
         float D):

    cdef Py_ssize_t i, j
    cdef float x, y

    cdef np.ndarray[DtypeA_t, ndim=2] NEW_ARRAY = np.zeros((len(C),3), dtype=DtypeA)

    for i in range(len(C)):
        if C[i] != 1:
            for j in range(i+1,len(C)):
                if A[i][0]==A[j][0] and A[i][1]==A[j][1] and C[j]!= 1:
                    x = B[i][0]+B[j][0]
                    y = B[i][1]+B[j][1]
                    C[i],C[j] = 1,1

                    if abs(y)/abs(x) > D:
                        if y > 0: NEW_ARRAY[i]=([A[i][0],A[i][1],y])

    return NEW_ARRAY

除此之外,我将之前的数组 "b" 拆分为两个不同的输入数组 "B" 和 "C",因为 "b" 的每一行包含 2 个 float 元素和一个仅充当标志的整数。所以我删除了标志整数并将它们放在一个单独的一维数组 "C" 中。所以,输入现在看起来像这样:

A =[[12.7, 13.5, 1.0],[23.4, 43.1, 1.0],...]
B =[[0.46,0.95],[4.56,0.92],...]
C =[0,0,...]
D = 0.1

理想情况下,现在输入所有变量应该会快得多(?)...但显然我做错了,因为函数现在在 35.3 秒时出现...比 "pure Python"!!

更糟糕

我到底在搞什么鬼?感谢阅读!

你有没有用cython -a手动编译过,并检查过逐行注解? (如果是这样,如果您可以 post 它的图像,或者写下它告诉您的内容,那将对我们有所帮助)。用黄色突出显示的行表示源到源转译器输出导致大量使用 CPython API.

的行

例如,您 cdef Py_ssize_t i, j 但没有理由不能是 C 整数。将它们视为 Py_ssize_t 需要开销,如果它们仅用作带边界的简单循环中的索引,您可以轻松保证,没有必要。我不会提出 Py_ssize_t 来试图说一般不要使用它。如果您的情况涉及需要支持索引的 64 位架构或更高精度的整数,那么当然可以使用它。我之所以提到它,是因为像这样的小事情有时会对 when/why 产生意想不到的重大影响,一堆 CPython API 的东西被捆绑到一些你认为会的 Cython 代码中摆脱 CPythonAPI。也许您的代码中一个更好的示例是在循环内使用 Python bool 值和 and 的构造,而不是这些的向量化或 C 级版本。

Cython 中的此类位置通常指的是您不会获得加速并且经常会减速的位置,特别是如果它们与 NumPy 代码混合在一起,由于其对 Cython / 扩展包装器的更紧密优化使用,否则就不必处理 CPython API 开销。您可以让 cython -a 输出指导您添加 C 级类型声明以替换 Python 类型,或者使用 C 级函数(例如来自 C 数学库)而不是 Python 可能需要处理参数的操作,即使是键入时,也可能是任何类型的 Python 对象,以及这涉及的所有许多属性查找和调度调用。

我相信索引符号 b[j][0] 的使用可能会影响 Cython,使其无法在幕后使用快速索引操作。顺便说一句,即使在纯 Python 代码中,这种风格也不是惯用的,可能会导致代码变慢。

尝试在整个过程中使用符号 b[j,0],看看它是否会提高您的表现。

显示带有 cython -a 的注释对于优化 Cython 代码确实非常有用。这是一个应该更快的版本,它使用更清晰的内存视图语法,

# cython: boundscheck=False
# cython: cdivision=True
# cython: wraparound=False

import numpy as np
cimport numpy as np


def loop(double [:,::1] A, double [:,::1] B, long [::1] C, float D):

    cdef Py_ssize_t i, j, N
    cdef float x, y

    N = len(C)

    cdef double[:,::1] NEW_ARRAY = np.zeros((N,3), dtype='float64')

    for i in range(N):
        if C[i] != 1:
            for j in range(i+1, N):
                if (A[i,0]==A[j,0]) & (A[i,1]==A[j,1]) & (C[j] != 1):
                    x = B[i,0] + B[j,0]
                    y = B[i,1] + B[j,1]
                    C[i] = 1
                    C[j] = 1

                    if abs(y/x) > D and y >0:
                        NEW_ARRAY[i,0] = A[i,0]
                        NEW_ARRAY[i,1] = A[i,1]
                        NEW_ARRAY[i,2] = y

    return NEW_ARRAY.base