【问题标题】:Numpy array multiply behavior is different from pure-Python to CythonNumpy 数组乘法行为不同于纯 Python 和 Cython
【发布时间】:2021-08-12 13:58:05
【问题描述】:

在纯 Python 代码中:

案例A:

retimg = np.zeros((dstH, dstW, 3), dtype=np.uint8)
A = img[x % (scrH - 1), y % (scrW - 1)]
B = img[x % (scrH - 1), y1 % (scrW - 1)]
C = img[x1 % (scrH - 1), y % (scrW - 1)]
D = img[x1 % (scrH - 1), y1 % (scrW - 1)]
retimg[i, j] = A * (1 - mu) * (1 - nu) + B * mu * (1 - nu) + C * (1 - mu) * nu + D * mu * nu

案例 B:

retimg = np.zeros((dstH, dstW, 3), dtype=np.uint8)
A = img[x % (scrH - 1), y % (scrW - 1)]
B = img[x % (scrH - 1), y1 % (scrW - 1)]
C = img[x1 % (scrH - 1), y % (scrW - 1)]
D = img[x1 % (scrH - 1), y1 % (scrW - 1)]
(r, g, b) = (
          A[0] * (1 - mu) * (1 - nu) + B[0] * mu * (1 - nu) + C[0] * (1 - mu) * nu + D[0] * mu * nu,
          A[1] * (1 - mu) * (1 - nu) + B[1] * mu * (1 - nu) + C[1] * (1 - mu) * nu + D[1] * mu * nu,
          A[2] * (1 - mu) * (1 - nu) + B[2] * mu * (1 - nu) + C[2] * (1 - mu) * nu + D[2] * mu * nu)
retimg[i, j] = (r, g, b)

Case ACase B 快​​很多

然后我使用 Cython 来加速执行。

案例 C:

cdef np.ndarray[DTYPEU8_t, ndim=3] dst = np.zeros((dstH, dstW, 3), dtype=np.uint8)
cdef np.ndarray[DTYPEU8_t, ndim=1] A,B,C,D
A = img[x % (scrH - 1), y % (scrW - 1)]
B = img[x % (scrH - 1), y1 % (scrW - 1)]
C = img[x1 % (scrH - 1), y % (scrW - 1)]
D = img[x1 % (scrH - 1), y1 % (scrW - 1)]
retimg[i, j] = A * (1 - mu) * (1 - nu) + B * mu * (1 - nu) + C * (1 - mu) * nu + D * mu * nu

案例 D:

cdef np.ndarray[DTYPEU8_t, ndim=3] dst = np.zeros((dstH, dstW, 3), dtype=np.uint8)
cdef float r,g,b
cdef np.ndarray[DTYPEU8_t, ndim=1] A,B,C,D
A = img[x % (scrH - 1), y % (scrW - 1)]
B = img[x % (scrH - 1), y1 % (scrW - 1)]
C = img[x1 % (scrH - 1), y % (scrW - 1)]
D = img[x1 % (scrH - 1), y1 % (scrW - 1)]
(r, g, b) = (
                A[0] * (1 - mu) * (1 - nu) + B[0] * mu * (1 - nu) + C[0] * (1 - mu) * nu + D[0] * mu * nu,
                A[1] * (1 - mu) * (1 - nu) + B[1] * mu * (1 - nu) + C[1] * (1 - mu) * nu + D[1] * mu * nu,
                A[2] * (1 - mu) * (1 - nu) + B[2] * mu * (1 - nu) + C[2] * (1 - mu) * nu + D[2] * mu * nu)

retimg[i, j] = (r, g, b)

Case CCase D 慢很多

为什么 Numpy 乘法数组在 Python 和 Cython 中的行为不同?理论上Case C 应该比Case D 快。

【问题讨论】:

  • “理论上案例 C 应该比案例 D 更快” - 为什么? (当然,您可能会从案例 D 中排除一些重复的术语,但除此之外......)

标签: python python-3.x performance numpy cython


【解决方案1】:

在 Cython 中键入 np.ndarray 的唯一作用是更快地索引单个元素。数组切片、整数组操作(如*+)等Numpy函数调用不加速。

对于案例 D,A[0]B[0]C[0]A[1] 等被有效索引并直接与 C 浮点数相乘,因此此计算非常快。相反,在案例 C 中,您有一堆数组乘法,它们作为普通的 Python 函数调用进行。由于数组很小(3 个元素长),Python 函数调用的成本很高。

retimg[i, j] = (r, g, b) 最好写成:

retimg[i,j,0] = r
retimg[i,j,1] = g
retimg[i,j,2] = b

利用索引(即 Cython 做得好的地方)。不过,Cython 可能会自然而然地优化它(但可能不会那么远)。


总而言之:输入np.ndarray 是没有意义的,除非您正在执行单元素索引。如果您不这样做,实际上会浪费时间进行不必要的类型检查。

【讨论】:

    【解决方案2】:

    这里Case CCase D慢的原因是临时变量的类型。实际上,在Case C 中,许多临时数组 被隐式创建和删除。这会导致大量内存分配。相对于 CPython 解释器,内存分配是相当快的。但是,当使用 Cython 优化代码时,分配速度非常慢,因为它们比飞行乘法要慢得多。此外,使用 Cython,标量表达式可以优化,因此它们使用 处理器寄存器,而基于数组的表达式通常不会优化并使用 慢内存层次结构(因为这很难去做)。更不用说 Numpy 调用可能会增加额外的大量开销。

    在我的机器上,1 次分配/解除分配的成本比计算完整表达式花费的时间更多。

    避免分配的一种解决方案是向 Numpy 指定数组的目的地,并尽可能避免基于数组的临时操作。这是一个未经测试的示例:

    # tmp is a predefined temporary array and res the resulting array
    np.multiply(A, (1 - mu) * (1 - nu), out=res)
    np.multiply(B, mu * (1 - nu), out=tmp)
    np.add(tmp, res, out=res)
    np.multiply(C, (1 - mu) * nu, out=tmp)
    np.add(tmp, res, out=res)
    np.multiply(D, mu * nu, out=tmp)
    np.add(tmp, res, out=res)
    

    请注意,上述解决方案并不能解决问题(与寄存器的使用和 Numpy 的开销有关),而 Case D 应该解决这些问题。

    【讨论】:

    • 感谢 Richard。我在我的机器上测试了你的代码。它比Case C 快,比Case D 慢。确实好像很多临时数组都是隐式创建和删除的
    猜你喜欢
    • 1970-01-01
    • 2014-03-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-12-13
    • 1970-01-01
    • 2015-10-12
    相关资源
    最近更新 更多