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 A
比 Case 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 C
比 Case D
慢很多
为什么 Numpy 乘法数组的行为与 Python 与 Cython 不同?理论上 Case C
应该比 Case D
.
快
这里Case C
比Case 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
是没有意义的,除非您正在执行单元素索引。如果你不这样做,它实际上会浪费时间进行不必要的类型检查。
在纯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 A
比 Case 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 C
比 Case D
为什么 Numpy 乘法数组的行为与 Python 与 Cython 不同?理论上 Case C
应该比 Case D
.
这里Case C
比Case 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
是没有意义的,除非您正在执行单元素索引。如果你不这样做,它实际上会浪费时间进行不必要的类型检查。