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 时那么快?
正如您所知,黄线意味着与 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\nRun 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)\nRun Code Online (Sandbox Code Playgroud)\n\n所以基本上我们得到一个新的切片(它是一个 numpy 数组),然后使用 numpy 的功能 ( PyObject_SetItem) 将所有元素设置为0.0,这是底层的 C 代码。
我们来看看手写for循环的版本:
\n\ncimport 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\nRun Code Online (Sandbox Code Playgroud)\n\n它仍然使用PyObject_Length(因为length)和边界检查,但除此之外它是 C 代码。当我们比较时间时:
>>> 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\nRun 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\nRun Code Online (Sandbox Code Playgroud)\n\n在生成的 html 中我们可以看到,这 a[i]变得非常简单:
__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 }\nRun Code Online (Sandbox Code Playgroud)\n\n__Pyx_BufPtrStrided1d(type, buf, i0, s0)是为 定义的(type)((char*)buf + i0 * s0)。\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\nRun 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\nRun 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\nRun Code Online (Sandbox Code Playgroud)\n\n所以它有点快,但对于更大的数组,你仍然无法击败 numpy。
\n\n在我看来,有两点值得借鉴:
\n\n最后一次尝试使用memset函数:
%%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))\nRun 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\nRun Code Online (Sandbox Code Playgroud)\n\n它也与大型数组的 numpy 代码一样快。
\n\n正如 DavidW 所建议的,人们可以尝试使用内存视图:
\n\n%%cython\ncimport numpy as np\ndef use_slices_memview(double[::1] a):\n a[0:len(a)]=0.0\nRun 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\nRun 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;\nRun 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;\nRun Code Online (Sandbox Code Playgroud)\n\n我希望不同的编译器以不同的方式处理这段代码。但显然,第一个版本更容易优化。
\n\ndouble[:] a正如 Behzad Jamali 指出的,和 之间存在差异double[::1] a。使用切片的第二个版本在我的机器上大约快了 20%。不同之处在于,在编译时,版本已知double[::1],内存访问将是连续的,这可以用于优化。在版本中,double[:]我们直到运行时才知道有关步幅的任何信息。