【问题标题】:I've mangled Cython badly, it's performing worse than pure Python. Why?我严重破坏了 Cython,它的性能比纯 Python 差。为什么?
【发布时间】:2015-05-25 15:38:44
【问题描述】:

我对 Python 比较陌生,对 C 完全一无所知(不幸的是),所以我很难正确理解使用 Cython 的某些方面。

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

在 Python 主程序中,我向函数传递了两个二维数组(“a”和“b”)和一个浮点数“d”,它返回一个列表“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

在“纯 Python”中,这在大约 12.5 秒内运行(有数万次循环)。在 Cython 中,它的运行时间约为 6.3 秒。近乎为零的工作取得了巨大进展!

但是,稍微阅读一下,很明显可以做很多很多事情,所以我开始尝试应用一些类型更改以使事情进展得更快,遵循 Cython 文档,here(也被引用在 cmets 中)。

以下是收集的修改,旨在模仿 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 个浮点元素和一个仅用作标志的整数。所以我删除了标志整数并将它们放在一个单独的一维数组“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.3s 的时间出现...更糟糕的是比“纯 Python”!!

我搞砸了什么?感谢阅读!

【问题讨论】:

  • 你读过这个:docs.cython.org/src/tutorial/numpy.html 吗?
  • 是的,我尽我所能模仿那里的建议,但我不确定如何处理附加列表...
  • 我会试着弄清楚代码到底做了什么,但我现在猜测使用向量化的 numpy 操作可能会有所帮助
  • 你能把你的数据发布到某个地方吗?
  • 我建议进行一些算法改进。例如,按字典顺序对a 进行排序将显示a 的哪些行相等,从而避免代价高昂的双循环,并可能让您摆脱似乎用来标记行的b 列已处理。

标签: python arrays list cython


【解决方案1】:

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

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

【讨论】:

  • 哇,是的,这大大提高了性能……后一个版本的代码现在运行时间为 0.2 秒!但是,如果我以同样的方式修改之前的代码,我会收到错误:“TypeError: list indices must be integers, not tuple” for line "if b[i][2] != 1:"
  • @Nordlendingen:这种索引方式不适用于列表列表,因为它们本质上不是二维数组。对于这种类型的代码,无论如何都可以使用 numpy ndarrays
【解决方案2】:

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

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

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

【讨论】:

  • 嗯,虽然您对使用cython -a 的建议很好,但Py_ssize_t 不只是typedef 用于能够装入单个通用寄存器的内置C 整数类型吗?这似乎不是一个巨大的性能问题......
  • 我试图澄清从 Py_ssize_t 到 C int 的优化是完全次要的(它只是降低了可能不需要的精度)。我的观点是,像这样的小事情会产生意想不到的巨大影响。我还指出在循环中使用and,需要PyObject 评估,这可能也不是什么大不了的事,但在阅读代码时似乎无害,但可以扩展为大量C-API - 依赖代码。
【解决方案3】:

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

【讨论】:

  • rth 和@Mr. F 感谢非常有用的建议和代码清理。非常感谢!
  • @Nordlendingen 顺便说一句,您能用您的数据为上述解决方案计时吗?这不仅仅是代码清理:memoryviews 应该比您在问题中使用的数组接口更快,并且标头中的 cython 指令消除了不必要的开销。
  • 是的,确实,您的解决方案再次将运行时间减半。仅修复索引符号就可以将其降低到 0.28 秒……但是您的代码将其进一步降低到 0.13 秒。再次感谢您提供真正有用的见解。
  • 我希望您可以通过取消使用 and 并在 C 值上使用按位运算来获得更多的加速。使用and(以及在这些逻辑操作中评估为boolPython 表达式),您会导致需要涉及大量 CPython API(例如,实际生成一个临时的bool 对象来保存A[i,0] == A[j, 0] 的值,然后在不需要时调度它的__and__ 特殊方法。
  • @rth:您可能要记住,使用本机代码,有时您在条件分支上浪费的时间比您不必评估其余条件所节省的时间还要多,那您可能会遇到的管道停顿以及所有...
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-12-25
  • 2011-03-01
  • 1970-01-01
  • 2018-07-29
  • 2019-05-01
  • 1970-01-01
相关资源
最近更新 更多