【问题标题】:Little speedup when porting Python code to Cython将 Python 代码移植到 Cython 时几乎没有加速
【发布时间】:2018-10-09 20:06:14
【问题描述】:

我有一些使用 numpy 计算函数梯度的 python 代码,这是我的应用程序中的一个大瓶颈。所以,我最初的尝试是尝试使用Cython 来提高性能。

因此,使用在线指南,我能够轻松地将其移植到 Cython,但速度非常适中,大约为 15%。该函数包含许多循环,我希望 Cython 能提供更好的改进。

Cython 代码如下所示。以下是仅从 Cython 调用的辅助函数。

cimport numpy as np
cimport cython

@cython.boundscheck(False)
@cython.wraparound(False)
@cython.nonecheck(False)
cdef cget_cubic_bspline_weight(double u):
    u = fabs(u)
    if u < 2.0:
        if u < 1.0:
            return 2.0 / 3.0 - u ** 2 + 0.5 * u ** 3
        else:
            return ((2.0 - u) ** 3) / 6.0

    return 0.0

@cython.boundscheck(False)
@cython.wraparound(False)
@cython.nonecheck(False)
cdef cget_cubic_spline_first_der_weight(double u):
    cdef double o = u
    u = fabs(u)
    cdef double v
    if u < 2.0:
        if u < 1.0:
            return (1.5 * u - 2.0) * o
        else:
            u -= 2.0
            v = -0.5 * u * u
            if o < 0.0:
                return -v
            return v

    return 0.0;

以下是计算梯度的主要函数。

@cython.boundscheck(False)
@cython.wraparound(False)
@cython.nonecheck(False)
cpdef gradient_2d(np.ndarray[double, ndim=2, mode="c"] reference,
                  np.ndarray[double, ndim=2, mode="c"] warped,
                  np.ndarray[double, ndim=5, mode="fortran"] warped_gradient,
                  np.ndarray[double, ndim=5, mode="fortran"] result_gradient,
                  double[:] entropies,
                  np.ndarray[double, ndim=2, mode="c"] jhlog,
                  np.ndarray[double, ndim=2, mode="fortran"] reflog,
                  np.ndarray[double, ndim=2, mode="fortran"] warlog,
                  int[:] bins,
                  int height, int width):

    war_x = warped_gradient[..., 0]
    war_y = warped_gradient[..., 1]

    res_x = result_gradient[..., 0]
    res_y = result_gradient[..., 1]
    nmi = (entropies[0] + entropies[1]) / entropies[2]

    for y in range(height):
        for x in range(width):
            ref = reference[x, y]
            war = warped[x, y]
            jd = [0.0] * 2
            rd = [0.0] * 2
            wd = [0.0] * 2

            for r in range(int(ref - 1.0), int(ref + 3.0)):
                if (-1 < r and r < bins[0]):
                    for w in range(int(war - 1.0), int(war + 3.0)):
                        if (-1 < w and w < bins[1]):
                            c = cget_cubic_bspline_weight(ref - float(r)) * \
                        cget_cubic_spline_first_der_weight(war - float(w))

                            jl = jhlog[r, w]
                            rl = reflog[r, 0]
                            wl = warlog[0, w]

                            jd[0] += c * war_x[x, y] * jl
                            rd[0] += c * war_x[x, y] * rl
                            wd[0] += c * war_x[x, y] * wl

                            jd[1] += c * war_y[x, y] * jl
                            rd[1] += c * war_y[x, y] * rl
                            wd[1] += c * war_y[x, y] * wl


            res_x[x, y] = (rd[0] + wd[0] - nmi * jd[0]) / (entropies[2] * entropies[3])
            res_y[x, y] = (rd[1] + wd[1] - nmi * jd[1]) / (entropies[2] * entropies[3])

现在,我称之为:

speed.gradient_2d(self.rdata, self.wdata, warped_grad_image,
                  result_gradient.data, self.entropies,
                  self.jhlog, self.reflog, self.warlog, self.bins,
                  int(self.rdata.shape[1]), int(self.rdata.shape[0]))

除了最后 2 个参数之外的所有参数都是 numpy 数组,并且如 cython 函数签名中所述。 python代码几乎相同,如果你愿意,我可以发布它,但它基本上是一样的。

我用setup.py 编译了整个东西:

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

ext = Extension("speed",
                sources=["perf/speed.pyx"],
                include_dirs=[numpy.get_include()],
                language="c++",
                libraries=[],
                extra_link_args=[])

setup(ext_modules = cythonize([ext]))

再一次,因为我的代码中有很多循环,我的印象是 Cython 版本会快得多,但我只得到了 15% 的改进。我按照本指南进行实施:http://docs.cython.org/en/latest/src/userguide/numpy_tutorial.html,据我所知,我做了它推荐的几乎所有事情。任何关于我下一步可以尝试的建议将不胜感激!

【问题讨论】:

  • 您应该分解/预计算一些值,以便它们只计算一次。也许在codereview.stackexchange.com 上试一试,看看是否有人想尝试优化您的代码
  • @Jean-FrançoisFabre 感谢您对 codereview 网站的建议。我实现了一个 C/C++ 等价物,而且速度要快得多,所以这就是为什么我很困惑为什么 cython 慢得多。我会在那里发帖。
  • 我想 Cython 仍然执行数组边界检查,这有点解释。生成的代码不如手动编写的 C 代码。
  • 不,我已将边界检查设置为关闭:@cython.boundscheck(False) @cython.wraparound(False) @cython.nonecheck(False)
  • 我不是 cython 专家,但访问列表项使用函数调用,而 a[i] 只是 C 的整数指针加法。我可能完全错了,因为我不知道 Cython .也许编辑您的帖子以显示 cython 生成的 C 代码?

标签: python performance numpy cython cythonize


【解决方案1】:

好的,玩了一会儿之后,事实证明,提高速度的主要方法是使用 ctypes。这是修改后的代码,它提供了大约 13 倍的加速。我把它留在这里,以防它对其他人有用。我确信可以提取更多性能,但我会遇到收益递减。

@cython.boundscheck(False)
@cython.wraparound(False)
@cython.nonecheck(False)
cpdef gradient_2d(np.ndarray[double, ndim=2, mode="c"] reference,
                  np.ndarray[double, ndim=2, mode="c"] warped,
                  np.ndarray[double, ndim=5, mode="fortran"] warped_gradient,
                  np.ndarray[double, ndim=5, mode="fortran"] result_gradient,
                  double[:] entropies,
                  np.ndarray[double, ndim=2, mode="c"] jhlog,
                  np.ndarray[double, ndim=2, mode="fortran"] reflog,
                  np.ndarray[double, ndim=2, mode="fortran"] warlog,
                  int[:] bins,
                  int height, int width):

    # As per @DavidW suggestion. See comment below.

    cdef np.ndarray[double, ndim=4, mode="fortran"] war_x = warped_gradient[..., 0]
    cdef np.ndarray[double, ndim=4, mode="fortran"] war_y = warped_gradient[..., 1]

    cdef np.ndarray[double, ndim=4, mode="fortran"] res_x = result_gradient[..., 0]
    cdef np.ndarray[double, ndim=4, mode="fortran"] res_y = result_gradient[..., 1]
    cdef double nmi = (entropies[0] + entropies[1]) / entropies[2]
    cdef double norm = entropies[2] * entropies[3]

    cdef double jd[2]
    cdef double rd[2]
    cdef double wd[2]

    cdef double ref
    cdef double war
    cdef double c_war_x_x_y
    cdef double c_war_y_x_y

    cdef double jl
    cdef double rl
    cdef double wl

    for y in range(height):
        for x in range(width):
            ref = reference[x, y]
            war = warped[x, y]

            jd[0] = jd[1] = 0.0
            rd[0] = rd[1] = 0.0
            wd[0] = wd[1] = 0.0

            for r in range(int(ref - 1.0), int(ref + 3.0)):
                if (-1 < r and r < bins[0]):
                    for w in range(int(war - 1.0), int(war + 3.0)):
                        if (-1 < w and w < bins[1]):
                            c = cget_cubic_bspline_weights(ref - r) * \
                            cget_cubic_spline_first_der_weights(war - w)
                            jl = jhlog[r, w]
                            rl = reflog[r, 0]
                            wl = warlog[0, w]

                            c_war_x_x_y = c * war_x[x, y]
                            c_war_y_x_y = c * war_y[x, y]

                            jd[0] += c_war_x_x_y * jl
                            rd[0] += c_war_x_x_y * rl
                            wd[0] += c_war_x_x_y * wl

                            jd[1] += c_war_y_x_y * jl
                            rd[1] += c_war_y_x_y * rl
                            wd[1] += c_war_y_x_y * wl


            res_x[x, y] = (rd[0] + wd[0] - nmi * jd[0]) / norm
            res_y[x, y] = (rd[1] + wd[1] - nmi * jd[1]) / norm

【讨论】:

  • 我也很想输入war_xwar_y(可能还有res_xres_y)作为合适的数组类型。我怀疑你会从中获得一些可衡量的速度。
  • @DavidW 再加速 4 倍!我已经用建议更新了答案!
  • 术语“ctypes”在 Python 上下文中有点误导。我称它们为“c 类型”或“cdef 变量”。
猜你喜欢
  • 2023-03-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-01-29
  • 2016-03-30
  • 1970-01-01
相关资源
最近更新 更多