Cython:了解 html 注释文件的内容吗?

Beh*_*ali 3 python numpy cython

编译以下 Cython 代码后,我得到如下所示的 html 文件:

import numpy as np
cimport numpy as np

cpdef my_function(np.ndarray[np.double_t, ndim = 1] array_a,
                  np.ndarray[np.double_t, ndim = 1] array_b,
                  int n_rows,
                  int n_columns):

    array_a[0:-1:n_columns] = 0
    array_a[n_columns - 1:n_rows * n_columns:n_columns] = 0
    array_a[0:n_columns] = 0
    array_a[n_columns* (n_rows - 1):n_rows * n_columns] = 0
    array_b[array_a == 3] = 0

    return array_a, array_b
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

我的问题是,为什么我的函数的那些操作仍然是黄色的?这是否意味着代码仍然没有使用 Cython 时那么快?

ead*_*ead 5

正如您所知,黄线意味着与 python 发生了一些交互,即使用了 python 功能而不是原始 c 功能,您可以查看生成的代码以查看发生的情况以及是否可以/应该修复/避免。

\n\n

并非每次与 python 的交互都意味着(可测量的)速度减慢。

\n\n

让我们看一下这个简化的函数:

\n\n
%%cython\ncimport numpy as np\ndef use_slices(np.ndarray[np.double_t] a):\n    a[0:len(a)]=0.0\n
Run Code Online (Sandbox Code Playgroud)\n\n

当我们查看生成的代码时,我们会看到(我只保留了重要的部分):

\n\n
  __pyx_t_1 = PyObject_Length(((PyObject *)__pyx_v_a)); \n  __pyx_t_2 = PyInt_FromSsize_t(__pyx_t_1); \n  __pyx_t_3 = PySlice_New(__pyx_int_0, __pyx_t_2, Py_None); \n  PyObject_SetItem(((PyObject *)__pyx_v_a)\n
Run Code Online (Sandbox Code Playgroud)\n\n

所以基本上我们得到一个新的切片(它是一个 numpy 数组),然后使用 numpy 的功能 ( PyObject_SetItem) 将所有元素设置为0.0,这是底层的 C 代码。

\n\n

我们来看看手写for循环的版本:

\n\n
cimport numpy as np\ndef use_for(np.ndarray[np.double_t] a):\n    cdef int i\n    for i in range(len(a)):\n        a[i]=0.0\n
Run Code Online (Sandbox Code Playgroud)\n\n

它仍然使用PyObject_Length(因为length)和边界检查,但除此之外它是 C 代码。当我们比较时间时:

\n\n
>>> import numpy as np\n>>> a=np.ones((500,))\n>>> %timeit use_slices(a)\n100000 loops, best of 3: 1.85 \xc2\xb5s per loop\n>>> %timeit use_for(a)\n1000000 loops, best of 3: 1.42 \xc2\xb5s per loop\n\n>>> b=np.ones((250000,))\n>>> %timeit use_slices(b)\n10000 loops, best of 3: 104 \xc2\xb5s per loop\n>>> %timeit use_for(b)\n1000 loops, best of 3: 364 \xc2\xb5s per loop\n
Run Code Online (Sandbox Code Playgroud)\n\n

您可以看到创建小尺寸切片的额外开销,但 for 版本中的额外检查意味着从长远来看它会产生更多开销。

\n\n

让我们禁用这些检查:

\n\n
%%cython\ncimport cython\ncimport numpy as np\n@cython.boundscheck(False)\n@cython.wraparound(False)\ndef use_for_no_checks(np.ndarray[np.double_t] a):\n    cdef int i\n    for i in range(len(a)):\n        a[i]=0.0\n
Run Code Online (Sandbox Code Playgroud)\n\n

在生成的 html 中我们可以看到,这 a[i]变得非常简单:

\n\n
 __pyx_t_3 = __pyx_v_i;\n    *__Pyx_BufPtrStrided1d(__pyx_t_5numpy_double_t *, __pyx_pybuffernd_a.rcbuffer->pybuffer.buf, __pyx_t_3, __pyx_pybuffernd_a.diminfo[0].strides) = 0.0;\n  }\n
Run Code Online (Sandbox Code Playgroud)\n\n

__Pyx_BufPtrStrided1d(type, buf, i0, s0)是为 定义的(type)((char*)buf + i0 * s0)。\n现在:

\n\n
>>> %timeit use_for_no_checks(a)\n1000000 loops, best of 3: 1.17 \xc2\xb5s per loop\n>>> %timeit use_for_no_checks(b)\n1000 loops, best of 3: 246 \xc2\xb5s per loop\n
Run Code Online (Sandbox Code Playgroud)\n\n

我们可以通过在 for 循环中释放 gil 来进一步改进它:

\n\n
%%cython\ncimport cython\ncimport numpy as np\n@cython.boundscheck(False)\n@cython.wraparound(False)\ndef use_for_no_checks_no_gil(np.ndarray[np.double_t] a):\n    cdef int i\n    cdef int n=len(a)\n    with nogil:\n      for i in range(n):\n        a[i]=0.0\n
Run Code Online (Sandbox Code Playgroud)\n\n

现在:

\n\n
>>> %timeit use_for_no_checks_no_gil(a)\n1000000 loops, best of 3: 1.07 \xc2\xb5s per loop\n>>> %timeit use_for_no_checks_no_gil(b)\n10000 loops, best of 3: 166 \xc2\xb5s per loop\n
Run Code Online (Sandbox Code Playgroud)\n\n

所以它有点快,但对于更大的数组,你仍然无法击败 numpy。

\n\n

在我看来,有两点值得借鉴:

\n\n
    \n
  1. Cython 不会通过 for 循环将切片转换为访问,因此必须使用 Python 功能。
  2. \n
  3. 开销很小,但它只是调用 numpy 功能,大部分工作是在 numpy 代码中完成的,并且不能通过 Cython 加速。
  4. \n
\n\n
\n\n

最后一次尝试使用memset函数:

\n\n
%%cython\nfrom libc.string cimport memset\ncimport numpy as np\ndef use_memset(np.ndarray[np.double_t] a):\n    memset(&a[0], 0, len(a)*sizeof(np.double_t))\n
Run Code Online (Sandbox Code Playgroud)\n\n

我们得到:

\n\n
>>> %timeit use_memset(a)\n1000000 loops, best of 3: 821 ns per loop\n>>> %timeit use_memset(b)\n10000 loops, best of 3: 102 \xc2\xb5s per loop\n
Run Code Online (Sandbox Code Playgroud)\n\n

它也与大型数组的 numpy 代码一样快。

\n\n
\n\n

正如 DavidW 所建议的,人们可以尝试使用内存视图:

\n\n
%%cython\ncimport numpy as np\ndef use_slices_memview(double[::1] a):\n    a[0:len(a)]=0.0\n
Run Code Online (Sandbox Code Playgroud)\n\n

导致小数组的代码稍快一些,但大数组的代码也类似(与 numpy-slices 相比):

\n\n
>>> %timeit use_slices_memview(a)\n1000000 loops, best of 3: 1.52 \xc2\xb5s per loop\n\n>>> %timeit use_slices_memview(b)\n10000 loops, best of 3: 105 \xc2\xb5s per loop\n
Run Code Online (Sandbox Code Playgroud)\n\n

这意味着内存视图切片的开销比 numpy 切片少。这是生成的代码:

\n\n
 __pyx_t_1 = __Pyx_MemoryView_Len(__pyx_v_a); \n  __pyx_t_2.data = __pyx_v_a.data;\n  __pyx_t_2.memview = __pyx_v_a.memview;\n  __PYX_INC_MEMVIEW(&__pyx_t_2, 0);\n  __pyx_t_3 = -1;\n  if (unlikely(__pyx_memoryview_slice_memviewslice(\n    &__pyx_t_2,\n    __pyx_v_a.shape[0], __pyx_v_a.strides[0], __pyx_v_a.suboffsets[0],\n    0,\n    0,\n    &__pyx_t_3,\n    0,\n    __pyx_t_1,\n    0,\n    1,\n    1,\n    0,\n    1) < 0))\n{\n    __PYX_ERR(0, 27, __pyx_L1_error)\n}\n\n{\n      double __pyx_temp_scalar = 0.0;\n      {\n          Py_ssize_t __pyx_temp_extent = __pyx_t_2.shape[0];\n          Py_ssize_t __pyx_temp_idx;\n          double *__pyx_temp_pointer = (double *) __pyx_t_2.data;\n          for (__pyx_temp_idx = 0; __pyx_temp_idx < __pyx_temp_extent; __pyx_temp_idx++) {\n            *((double *) __pyx_temp_pointer) = __pyx_temp_scalar;\n            __pyx_temp_pointer += 1;\n          }\n      }\n  }\n  __PYX_XDEC_MEMVIEW(&__pyx_t_2, 1);\n  __pyx_t_2.memview = NULL;\n  __pyx_t_2.data = NULL;\n
Run Code Online (Sandbox Code Playgroud)\n\n

我认为最重要的部分:此代码不会创建额外的临时对象 - 它重用切片的现有内存视图。

\n\n

如果使用内存视图,我的编译器会生成(至少对于我的机器)稍快的代码。不确定是否值得调查。乍一看,每个迭代步骤的差异是:

\n\n
# created code for memview-slices:\n*((double *) __pyx_temp_pointer) = __pyx_temp_scalar;\n __pyx_temp_pointer += 1;\n\n#created code for memview-for-loop:\n __pyx_v_i = __pyx_t_3;\n __pyx_t_4 = __pyx_v_i;\n *((double *) ( /* dim=0 */ ((char *) (((double *) data) + __pyx_t_4)) )) = 0.0;\n
Run Code Online (Sandbox Code Playgroud)\n\n

我希望不同的编译器以不同的方式处理这段代码。但显然,第一个版本更容易优化。

\n\n
\n\n

double[:] a正如 Behzad Jamali 指出的,和 之间存在差异double[::1] a。使用切片的第二个版本在我的机器上大约快了 20%。不同之处在于,在编译时,版本已知double[::1],内存访问将是连续的,这可以用于优化。在版本中,double[:]我们直到运行时才知道有关步幅的任何信息。

\n