Небольшое ускорение при переносе кода Python на Cython - PullRequest
0 голосов
/ 29 апреля 2018

У меня есть некоторый код на python, который использует numpy, который вычисляет градиент функции, и это большое узкое место в моем приложении. Итак, моей первоначальной попыткой было попытаться использовать 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]))

Все, кроме двух последних параметров, являются массивами-пустышками и соответствуют описанию в сигнатуре функции 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 и, насколько я могу судить, я сделал почти все, что он рекомендует. Буду очень признателен за любые предложения о том, что я мог бы попробовать дальше!

1 Ответ

0 голосов
/ 29 апреля 2018

Хорошо, после небольшой игры выясняется, что главное, что увеличит скорость, это использование 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
...