Numpy 数组乘法行为不同于 pure-Python 到 Cython

Numpy array multiply behavior is different from pure-Python to Cython

在纯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.

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

在我的机器上,1 allocation/deallocation 的成本比计算完整表达式花费的时间更多。

避免分配的一种解决方案是向 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 应该可以解决这些问题。

在 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 是没有意义的,除非您正在执行单元素索引。如果你不这样做,它实际上会浪费时间进行不必要的类型检查。