【问题标题】:Poor Cython performance with typed MemoryView键入 MemoryView 的 Cython 性能不佳
【发布时间】:2017-04-21 01:08:06
【问题描述】:

我正在尝试使用 Cython 加速一些纯 Python 代码。这是原始 Python 代码:

import numpy as np
def image_to_mblocks(image_component):
    img_shape = np.shape(image_component)
    v_mblocks = img_shape[0] // 16
    h_mblocks = img_shape[1] // 16
    x = image_component
    x = [x[i * 16:(i + 1) * 16:, j * 16:(j + 1) * 16:] for i in range(v_mblocks) for j in range(h_mblocks)]
    return x

参数image_component是一个二维的numpy.ndarray,其中每个维度的长度可以被16整除。在纯Python中,这个函数很快——在我的机器上,100次调用image_component形状(640, 480) 需要 80 毫秒。但是,我需要调用这个函数几千到几万次,所以我有兴趣加快它。

这是我的 Cython 实现:

import numpy as np
cimport numpy as np
cimport cython
ctypedef unsigned char DTYPE_pixel

cpdef np.ndarray[DTYPE_pixel, ndim=3] image_to_mblocks(unsigned char[:, :] image_component):

    cdef int i
    cdef int j
    cdef int k = 0
    cdef int v_mblocks = image_component.shape[0] / 16
    cdef int h_mblocks = image_component.shape[1] / 16
    cdef np.ndarray[DTYPE_pixel, ndim=3] x = np.empty((v_mblocks*h_mblocks, 16, 16), dtype=np.uint8)

    for j in range(h_mblocks):
        for i in range(v_mblocks):
            x[k] = image_component[i * 16:(i + 1) * 16:, j * 16:(j + 1) * 16:]
            k += 1
    return x

Cython 实现使用类型化的MemoryView 来支持image_component 的切片。这个 Cython 实现在我的机器上进行 100 次迭代需要 250 毫秒(与以前相同的条件:image_component 是一个(640, 480) 数组)。

这是我的问题:在我给出的示例中,为什么 Cython 无法胜过纯 Python 实现?

我相信我已按照Cython documentation for working with numpy arrays 中的所有步骤进行操作,但未能实现预期的性能提升。

作为参考,我的 setup.py 文件如下所示:

from distutils.core import setup
from distutils.extension import Extension
from Cython.Build import cythonize
import numpy

extensions = [
    Extension('proto_mpeg_computation', ['proto_mpeg_computation.pyx'],
          include_dirs=[numpy.get_include()]
          ),
]

setup(
   name = "proto_mpeg_x",
   ext_modules = cythonize(extensions)
)

【问题讨论】:

  • 查看 Cython 吐出的 C 代码通常很有帮助(如果烦人的话)。您可能会发现它犯了一个错误,没有使循环成为真正的 C 循环,数组索引不是真正的 C 数组索引操作等。一旦您知道 Cython -> C 编译器在搞砸什么,您会知道玩什么。例如,我发现带有 range 的复杂循环结构有时无法成为真正的 C 循环,将它们更改为旧 Pyrex 样式的 range 循环会强制进行正确的优化。
  • 类型化的内存视图应该加快单元素索引。我不希望切片速度有任何提高。 (奇怪的是你的速度明显放缓。我希望它会相似)
  • 检查的额外步骤:使用 Cython 的注释功能来查看正确的 cythonized,使用装饰器删除边界检查,使用 cdef double[::1] 类型声明。

标签: python numpy cython


【解决方案1】:

性能明显变差的原因是 Cython 版本正在复制数据,而原始版本正在创建对现有数据的引用。

线

x[i * 16:(i + 1) * 16:, j * 16:(j + 1) * 16:]

在原始 x 数组上创建一个视图(即,如果您更改 x,那么视图也会更改)。您可以通过检查 Python 函数返回的数组元素上的 numpy owndata 标志是 False 来确认这一点。这个操作非常便宜,因为它所做的只是存储一个指针和一些形状/步幅信息。

在你做的 Cython 版本中

x[k] = image_component[i * 16:(i + 1) * 16:, j * 16:(j + 1) * 16:]

这需要将一个 16 x 16 数组复制到已为 x 分配的内存中。它不是超慢的,但与原始 Python 版本相比,还有更多工作要做。再次,通过检查函数返回值上的owndata 来确认。你应该会发现它是True

在您的情况下,您应该考虑是否需要数据视图或数据副本。


在我看来,这不是 Cython 将有很大帮助的问题。 Cython 在索引单个元素方面有一些很好的加速,但是当您开始索引切片时,它的行为方式与基本 Python/numpy 相同(这对于这种类型的使用实际上非常有效)。

我怀疑将原始 Python 代码放入 Cython 并输入 image_componentunsigned char[:, :]np.ndarray[DTYPE_pixel, ndim=2] 会有所收获。您还可以通过不使用x 并直接返回列表推导来减少一点引用计数。除此之外,我看不出你能获得多少。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-05-18
    • 2016-09-07
    • 1970-01-01
    • 1970-01-01
    • 2019-06-21
    • 2012-09-29
    相关资源
    最近更新 更多