为什么访问 numpy 数组比使用 Cython 的 Pillow 图像慢 6 倍

Why accessing a numpy array is 6 times slower than Pillow image with Cython

我有一个 RGB 图像,其中每个像素都必须使用特殊公式重新计算,并且出于性能原因,我将 Cython 用于热循环。

我的代码正在处理传递给 Cython 代码的 Pillow Image 对象,其中每个像素都作为 image[x, y] = something 访问,它使用 Python __getitem__ 因此非常慢(我测量和get/set 项目占用了 50% 的功能时间)。

我的想法是使用 numpy 数组和可能的 Cython memory view 来加快速度,但是更改函数的新持续时间现在慢了 6 倍!!!

(顺便说一句,我使用的原始 calculate_color 函数比下面的示例复杂得多,所以不要让我使用 FOO 或 BAR 重写该片段。我正在尝试比较 r_001r_002)

代码main.py

from PIL import Image
import numpy as np
import pyximport
pyximport.install(setup_args={"include_dirs":np.get_include()})
import rendering

size = 2000, 2000
mode = 'RGB'
mult = np.ones(size[0] * size[1], np.long)

im_new = Image.new(mode, size)
rendering.r_001(im_new.load(), mult, size[0], size[1])

im_new = Image.new(mode, size)
rendering.r_002(np.array(im_new), mult, size[0], size[1])

代码rendering.pyx

cimport numpy as np
np.import_array()
import numpy as np

def r_001(object image, np.ndarray[long, ndim=1] multiplier, long w, long h):
    cdef long x, y, x_index, y_index

    for y from 0 <= y < h-1:
        y_index = w * y
        for x from 0 <= x < w-1:
            x_index = x + y_index
            m = multiplier[x_index]
            r, g, b = image[x, y]
            image[x, y] = calculate_color(m, r, g, b)


def r_002(np.ndarray[char, ndim=3] image, np.ndarray[long, ndim=1] multiplier, long w, long h):
    cdef long x, y, x_index, y_index

    for y from 0 <= y < h-1:
        y_index = w * y
        for x from 0 <= x < w-1:
            x_index = x + y_index
            m = multiplier[x_index]
            r, g, b = image[x, y]
            r, g, b = calculate_color(m, r, g, b)
            image[x, y] = <char>r, <char>g, <char>b


cdef inline tuple calculate_color(long m, long r, long g, long b):
    cdef long a = 75
    r = (a * m + r * m) // 256
    g = (a * m + g * m) // 256
    b = (a * m + b * m) // 256
    if r > 255: r = 255
    if g > 255: g = 255
    if b > 255: b = 255
    return r, g, b

使用 cProfile,我得到 r_001 需要 1.53 秒到 运行 而 r_002 需要 9.68 秒。

通过在代码上使用 cython -a 并查看带注释的 html,您可以很好地了解问题所在。本质上,问题行是:

r, g, b = image[x, y]
# ...
image[x, y] = <char>r, <char>g, <char>b

Cython 通常仅在对单个元素进行索引时速度较快 - 对于部分索引,它会返回到 __getitem__,然后(在本例中)元组解包和打包。重写代码的一种方法是:

r = image[x, y, 0]
g = image[x, y, 1]
b = image[x, y, 2]
# and equivalently for the assignment

您可以考虑通过 return a "ctuple":

来加快 calculate_color
cdef (char, char, char) calculate_color(... # as before ):

您还应该将 rgb(在 r_002 中)的类型设置为 char


如果是我,我可能会将 image 设为 32 位整数的二维数组,并使用位掩码获得单独的颜色。这将是对您的代码的更重要的更改,但会使索引更容易。


cProfile 是一种对单个函数进行计时的糟糕方法 - 根据内容,它会在调用时增加大量开销。这往往不是 Cython 的情况,因为它不能 "see inside" Cython 函数。这对于了解整个程序及其使用时间的位置很有帮助,但对于测量独立小块的性能,请改用 timeit 或类似的方法。