为什么此简单附加功能的Cython实现比Numba慢?

时间:2018-11-04 21:39:42

标签: python cython numba

我有2个版本的函数,可将行附加到2d数组中;一个在Cython,另一个在Numba。

Cython版本的性能比Numba版本慢很多。我想优化Cython版本,使其与Numba版本一样好。

我正在使用以下timer.py模块来计时代码: 导入时间

class Timer(object):
    def __init__(self, name='', output=print):
        self._name = name
        self._output = output

    def __enter__(self):
        self.start = time.time()
        return self

    def __exit__(self, a, b, c):
        self.end = time.time()
        self.time_taken = self.end - self.start
        self._output('%s Took %0.2fs seconds' % (self._name, self.time_taken))

我的append_2d_cython.pyx模块是:

#!python
#cython: boundscheck=False
#cython: wraparound=False


import numpy as np
cimport numpy as cnp

cnp.import_array()  # needed to initialize numpy-API


cpdef empty_2d(int d1, int d2):
    cdef:
        cnp.npy_intp[2] dimdim

    dimdim[0] = d1
    dimdim[1] = d2

    return cnp.PyArray_SimpleNew(2, dimdim, cnp.NPY_INT32)


cpdef append_2d(int[:, :] arr, int[:] value):

    cdef int[:, :] result

    result = empty_2d(arr.shape[0]+1, arr.shape[1])

    result[:arr.shape[0], :] = arr

    result[arr.shape[0], :] = value

    return result

我的append_2d_numba.py模块是:

import numba as nb
import numpy as np


@nb.jit(nopython=True)
def append_2d(arr, value):

    result = np.empty((arr.shape[0]+1, arr.shape[1]), dtype=arr.dtype)
    result[:-1] = arr
    result[-1] = value
    return result

我正在将append_2d的Numba和Cython版本与此脚本进行比较:

import pyximport
import numpy as np
pyximport.install(setup_args={'include_dirs': np.get_include()})

from timer import Timer
from append_2d_cython import append_2d as append_2d_cython
from append_2d_numba import append_2d as append_2d_numba

arr_2d = np.random.randint(0, 100, size=(5, 4), dtype=np.int32)
arr_1d = np.array([0, 1, 2, 3], np.int32)

num_tests = 100000

with Timer('append_2d_cython'):
    for _ in range(num_tests):
        r_cython = append_2d_cython(arr_2d, arr_1d)

# # JIT Compile it
append_2d_numba(arr_2d, arr_1d)

with Timer('append_2d_numba'):
    for _ in range(num_tests):
        r_numba = append_2d_numba(arr_2d, arr_1d)

哪些印刷品:

make many with cython Took 0.36s seconds
make many with numba Took 0.12s seconds

因此,对于此代码,numba比Cython快3倍。我想将Cython代码重构为与Numba代码一样快。我该怎么办?

1 个答案:

答案 0 :(得分:3)

此调查将显示,大量的Cython开销是Cython性能不佳的原因。此外,将提出一种(有点hacky)替代方案来避免大多数情况-因此,numba解决方案将被因素4击败。

让我们从在我的机器上建立基线开始(我将您的函数称为cy_append_2dnb_append_2d,并使用%timeit魔术来衡量运行时间):

arr_2d = np.arange(5*4, dtype=np.int32).reshape((5,4))
arr_1d = np.array([0, 1, 2, 3], np.int32)

%timeit cy_append_2d(arr_2d, arr_1d)
# 8.27 µs ± 141 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
%timeit nb_append_2d(arr_2d, arr_1d)
# 2.84 µs ± 169 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

Numba版本的速度快大约三倍-与您观察到的时间类似。

但是,我们必须知道,我们所测量的不是复制数据所需的时间,而是开销。并不是numba在做花哨的事情-它恰好具有较少的开销(但仍然很多-创建numpy数组和复制24个整数几乎需要3µs!)

如果我们增加复制数据的数量,我们将看到cython和numba的性能非常相似-没有任何出色的编译器可以大大改善复制:

N=5000
arr_2d_large = np.arange(5*N, dtype=np.int32).reshape((5,N))
arr_1d_large = np.arange(N, dtype=np.int32)
%timeit cy_append_2d(arr_2d_large, arr_1d_large)
# 35.7 µs ± 597 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
%timeit nb_append_2d(arr_2d_large, arr_1d_large)
# 44.8 µs ± 1.36 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

Cython的速度稍快一些,但是对于不同的机器和不同的尺寸,它可能会有所不同,出于我们的目的,我们可以认为它们几乎一样快。

正如@DavidW指出的那样,从numpy数组中的cython-ndarray中创建cython数组会带来相当大的开销。考虑一下这个虚拟函数:

%%cython
cpdef dummy(int[:, :] arr, int[:] value):
    pass

%timeit dummy(arr_2d, arr_1d)
# 3.24 µs ± 47.5 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

这意味着在开始执行该功能的第一个操作之前,原始的8µs 3中已经用完了-在这里您可以看到创建内存视图的成本。

通常,人们不会在乎这种开销-因为如果您为如此小的数据块调用numpy功能,那么性能将永远不会很出色。


但是,如果您确实喜欢这种微优化,则可以直接使用Numpy的C-API,而无需使用Cythons ndarray-helper。我们不能指望结果会像复制24个整数一样快-因为创建一个新的缓冲区/ numpy数组成本很高,但是我们击败8µs的机会非常高!

这是一个原型,它显示了可能的可能性:

%%cython
from libc.string cimport memcpy

# don't use Cythons wrapper, because it uses ndarray
# define only the needed stuff
cdef extern from "numpy/arrayobject.h":
    ctypedef int npy_intp            # it isn't actually int, but cython doesn't care anyway
    int _import_array() except -1
    char *PyArray_BYTES(object arr)
    npy_intp PyArray_DIM(object arr, int n)
    object PyArray_SimpleNew(int nd, npy_intp* dims, int typenum)
    cdef enum NPY_TYPES:
        NPY_INT32

# initialize Numpy's C-API when imported.
_import_array()  

def cy_fast_append_2d(upper, lower):
    # create resulting array:
    cdef npy_intp dims[2]
    dims[0] = PyArray_DIM(upper, 0)+1
    dims[1] = PyArray_DIM(upper, 1)
    cdef object res = PyArray_SimpleNew(2, &dims[0], NPY_INT32)
    # copy upper array, assume C-order/memory layout
    cdef char *dest = PyArray_BYTES(res)
    cdef char *source = PyArray_BYTES(upper)
    cdef int size = (dims[0]-1)*dims[1]*4 # int32=4 bytes
    memcpy(dest, source, size)
    # copy lower array, assume C-order/memory layout
    dest += size
    source = PyArray_BYTES(lower)
    size = dims[1]*4
    memcpy(dest, source, size)
    return res

现在是时间:

%timeit cy_fast_append_2d(arr_2d, arr_1d)
753 ns ± 3.13 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

这意味着Cython击败Numba的原因是4。

但是,这会损失很多安全性-例如,它仅适用于C阶数组,不适用于Fortran阶数组。但是,我的目标不是提供防水解决方案,而是研究直接使用Numpy的C-API可能会变得有多快-您的决定是否应该采用这种骇人听闻的方式。