为什么访问 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_001
和 r_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 ):
您还应该将 r
、g
和 b
(在 r_002
中)的类型设置为 char
。
如果是我,我可能会将 image
设为 32 位整数的二维数组,并使用位掩码获得单独的颜色。这将是对您的代码的更重要的更改,但会使索引更容易。
cProfile
是一种对单个函数进行计时的糟糕方法 - 根据内容,它会在调用时增加大量开销。这往往不是 Cython 的情况,因为它不能 "see inside" Cython 函数。这对于了解整个程序及其使用时间的位置很有帮助,但对于测量独立小块的性能,请改用 timeit
或类似的方法。
我有一个 RGB 图像,其中每个像素都必须使用特殊公式重新计算,并且出于性能原因,我将 Cython 用于热循环。
我的代码正在处理传递给 Cython 代码的 Pillow Image 对象,其中每个像素都作为 image[x, y] = something
访问,它使用 Python __getitem__
因此非常慢(我测量和get/set 项目占用了 50% 的功能时间)。
我的想法是使用 numpy 数组和可能的 Cython memory view 来加快速度,但是更改函数的新持续时间现在慢了 6 倍!!!
(顺便说一句,我使用的原始 calculate_color
函数比下面的示例复杂得多,所以不要让我使用 FOO 或 BAR 重写该片段。我正在尝试比较 r_001
和 r_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 ):
您还应该将 r
、g
和 b
(在 r_002
中)的类型设置为 char
。
如果是我,我可能会将 image
设为 32 位整数的二维数组,并使用位掩码获得单独的颜色。这将是对您的代码的更重要的更改,但会使索引更容易。
cProfile
是一种对单个函数进行计时的糟糕方法 - 根据内容,它会在调用时增加大量开销。这往往不是 Cython 的情况,因为它不能 "see inside" Cython 函数。这对于了解整个程序及其使用时间的位置很有帮助,但对于测量独立小块的性能,请改用 timeit
或类似的方法。