如何为类型化的内存视图分配内存是推荐的方式?

89

Cython文档中关于类型化内存视图,列出了三种给类型化内存视图分配值的方式:

  1. 使用原始C指针,
  2. 使用np.ndarray,以及
  3. 使用cython.view.array

假设我没有从外部传入数据到我的cython函数中,而是想要分配内存并将其作为np.ndarray返回,那么我应该选择哪个选项?还假设该缓冲区的大小不是编译时常量,即我无法在堆栈上分配,而需要使用malloc来选项1。

因此,这3个选项看起来像这样:

from libc.stdlib cimport malloc, free
cimport numpy as np
from cython cimport view

np.import_array()

def memview_malloc(int N):
    cdef int * m = <int *>malloc(N * sizeof(int))
    cdef int[::1] b = <int[:N]>m
    free(<void *>m)

def memview_ndarray(int N):
    cdef int[::1] b = np.empty(N, dtype=np.int32)

def memview_cyarray(int N):
    cdef int[::1] b = view.array(shape=(N,), itemsize=sizeof(int), format="i")
对我来说令人惊讶的是,在所有三种情况中,Cython为内存分配生成了相当多的代码,特别是对__Pyx_PyObject_to_MemoryviewSlice_dc_int的调用。这表明(我可能错了,在Cython内部工作方面我的见解非常有限)它首先创建一个Python对象,然后将其“转换”为内存视图,这似乎是不必要的开销。 一个简单的基准测试并没有揭示出三种方法之间的太大差异,其中第二种方法略微更快。
哪种方法被推荐?或者是否有不同、更好的选择?
跟进问题: 我最终希望将结果作为np.ndarray返回,经过函数处理过的内存视图是否是最佳选择,还是我最好像下面这样使用旧的缓冲区接口在一开始就创建ndarray?
cdef np.ndarray[DTYPE_t, ndim=1] b = np.empty(N, dtype=np.int32)

5
好问题,我也在想类似的事情。 - AlexE
你的基准测试是我所知道的最好的答案。为了回答后续问题,你可以按照通常的方式声明你的NumPy数组(甚至不必使用旧的类型接口),然后执行类似于cdef int[:] arrview = arr的操作,以获取与NumPy数组使用相同内存的视图。你可以使用该视图进行快速索引,并在Cython函数之间传递切片,同时仍然可以通过NumPy数组访问NumPy函数。完成后,你只需返回NumPy数组即可。 - IanH
这里有一个相关很好的问题,您可以看到 np.empty 可能会很慢... - Saullo G. P. Castro
2个回答

93

看这里答案在此.

基本思想是你需要使用 cpython.array.arraycpython.array.clone而不是 cython.array.*):

from cpython.array cimport array, clone

# This type is what you want and can be cast to things of
# the "double[:]" syntax, so no problems there
cdef array[double] armv, templatemv

templatemv = array('d')

# This is fast
armv = clone(templatemv, L, False)

编辑

事实证明,那个帖子中的基准测试是垃圾。这是我的数据集和时间:

# cython: language_level=3
# cython: boundscheck=False
# cython: wraparound=False

import time
import sys

from cpython.array cimport array, clone
from cython.view cimport array as cvarray
from libc.stdlib cimport malloc, free
import numpy as numpy
cimport numpy as numpy

cdef int loops

def timefunc(name):
    def timedecorator(f):
        cdef int L, i

        print("Running", name)
        for L in [1, 10, 100, 1000, 10000, 100000, 1000000]:
            start = time.clock()
            f(L)
            end = time.clock()
            print(format((end-start) / loops * 1e6, "2f"), end=" ")
            sys.stdout.flush()

        print("μs")
    return timedecorator

print()
print("INITIALISATIONS")
loops = 100000

@timefunc("cpython.array buffer")
def _(int L):
    cdef int i
    cdef array[double] arr, template = array('d')

    for i in range(loops):
        arr = clone(template, L, False)

    # Prevents dead code elimination
    str(arr[0])

@timefunc("cpython.array memoryview")
def _(int L):
    cdef int i
    cdef double[::1] arr
    cdef array template = array('d')

    for i in range(loops):
        arr = clone(template, L, False)

    # Prevents dead code elimination
    str(arr[0])

@timefunc("cpython.array raw C type")
def _(int L):
    cdef int i
    cdef array arr, template = array('d')

    for i in range(loops):
        arr = clone(template, L, False)

    # Prevents dead code elimination
    str(arr[0])

@timefunc("numpy.empty_like memoryview")
def _(int L):
    cdef int i
    cdef double[::1] arr
    template = numpy.empty((L,), dtype='double')

    for i in range(loops):
        arr = numpy.empty_like(template)

    # Prevents dead code elimination
    str(arr[0])

@timefunc("malloc")
def _(int L):
    cdef int i
    cdef double* arrptr

    for i in range(loops):
        arrptr = <double*> malloc(sizeof(double) * L)
        free(arrptr)

    # Prevents dead code elimination
    str(arrptr[0])

@timefunc("malloc memoryview")
def _(int L):
    cdef int i
    cdef double* arrptr
    cdef double[::1] arr

    for i in range(loops):
        arrptr = <double*> malloc(sizeof(double) * L)
        arr = <double[:L]>arrptr
        free(arrptr)

    # Prevents dead code elimination
    str(arr[0])

@timefunc("cvarray memoryview")
def _(int L):
    cdef int i
    cdef double[::1] arr

    for i in range(loops):
        arr = cvarray((L,),sizeof(double),'d')

    # Prevents dead code elimination
    str(arr[0])



print()
print("ITERATING")
loops = 1000

@timefunc("cpython.array buffer")
def _(int L):
    cdef int i
    cdef array[double] arr = clone(array('d'), L, False)

    cdef double d
    for i in range(loops):
        for i in range(L):
            d = arr[i]

    # Prevents dead-code elimination
    str(d)

@timefunc("cpython.array memoryview")
def _(int L):
    cdef int i
    cdef double[::1] arr = clone(array('d'), L, False)

    cdef double d
    for i in range(loops):
        for i in range(L):
            d = arr[i]

    # Prevents dead-code elimination
    str(d)

@timefunc("cpython.array raw C type")
def _(int L):
    cdef int i
    cdef array arr = clone(array('d'), L, False)

    cdef double d
    for i in range(loops):
        for i in range(L):
            d = arr[i]

    # Prevents dead-code elimination
    str(d)

@timefunc("numpy.empty_like memoryview")
def _(int L):
    cdef int i
    cdef double[::1] arr = numpy.empty((L,), dtype='double')

    cdef double d
    for i in range(loops):
        for i in range(L):
            d = arr[i]

    # Prevents dead-code elimination
    str(d)

@timefunc("malloc")
def _(int L):
    cdef int i
    cdef double* arrptr = <double*> malloc(sizeof(double) * L)

    cdef double d
    for i in range(loops):
        for i in range(L):
            d = arrptr[i]

    free(arrptr)

    # Prevents dead-code elimination
    str(d)

@timefunc("malloc memoryview")
def _(int L):
    cdef int i
    cdef double* arrptr = <double*> malloc(sizeof(double) * L)
    cdef double[::1] arr = <double[:L]>arrptr

    cdef double d
    for i in range(loops):
        for i in range(L):
            d = arr[i]

    free(arrptr)

    # Prevents dead-code elimination
    str(d)

@timefunc("cvarray memoryview")
def _(int L):
    cdef int i
    cdef double[::1] arr = cvarray((L,),sizeof(double),'d')

    cdef double d
    for i in range(loops):
        for i in range(L):
            d = arr[i]

    # Prevents dead-code elimination
    str(d)

输出:

INITIALISATIONS
Running cpython.array buffer
0.100040 0.097140 0.133110 0.121820 0.131630 0.108420 0.112160 μs
Running cpython.array memoryview
0.339480 0.333240 0.378790 0.445720 0.449800 0.414280 0.414060 μs
Running cpython.array raw C type
0.048270 0.049250 0.069770 0.074140 0.076300 0.060980 0.060270 μs
Running numpy.empty_like memoryview
1.006200 1.012160 1.128540 1.212350 1.250270 1.235710 1.241050 μs
Running malloc
0.021850 0.022430 0.037240 0.046260 0.039570 0.043690 0.030720 μs
Running malloc memoryview
1.640200 1.648000 1.681310 1.769610 1.755540 1.804950 1.758150 μs
Running cvarray memoryview
1.332330 1.353910 1.358160 1.481150 1.517690 1.485600 1.490790 μs

ITERATING
Running cpython.array buffer
0.010000 0.027000 0.091000 0.669000 6.314000 64.389000 635.171000 μs
Running cpython.array memoryview
0.013000 0.015000 0.058000 0.354000 3.186000 33.062000 338.300000 μs
Running cpython.array raw C type
0.014000 0.146000 0.979000 9.501000 94.160000 916.073000 9287.079000 μs
Running numpy.empty_like memoryview
0.042000 0.020000 0.057000 0.352000 3.193000 34.474000 333.089000 μs
Running malloc
0.002000 0.004000 0.064000 0.367000 3.599000 32.712000 323.858000 μs
Running malloc memoryview
0.019000 0.032000 0.070000 0.356000 3.194000 32.100000 327.929000 μs
Running cvarray memoryview
0.014000 0.026000 0.063000 0.351000 3.209000 32.013000 327.890000 μs

"迭代次数"基准测试的原因是某些方法在这方面具有惊人的不同特征。

按初始化速度排序:

malloc:虽然这是个严酷的世界,但速度很快。如果您需要分配大量内容并获得无阻碍迭代和索引性能,那么这必须是最佳选择。但通常情况下,您还有更好的选择...

cpython.array raw C type:该方法速度非常快,也很安全。不幸的是,它通过Python访问其数据字段。您可以通过使用奇妙的技巧来避免这种情况:

arr.data.as_doubles[i]

提升速度并取消安全性!这使得它成为优秀的malloc替代品,基本上是一个漂亮的引用计数版本!

cpython.array缓冲区:仅比malloc多3至4倍的设置时间,这看起来是一个很好的选择。不幸的是,它有相当大的开销(尽管与boundscheckwraparound指令相比很小)。这意味着它只真正与完全安全的变量竞争,但它是初始化最快的。你可以选择。

cpython.array内存视图:现在初始化比malloc 慢一个数量级。这很遗憾,但它的迭代速度与malloc相同。这是我建议的标准解决方案,除非打开了boundscheckwraparound(在这种情况下cpython.array缓冲区可能是更有吸引力的权衡)。

其余部分。唯一有价值的是numpy,因为附加了许多有趣的对象方法。就这些。


1
感谢你进行了如此全面的调查,并用数字加以支持! - kynan
3
好的回答!我认为只有使用纯malloc方案才能完全避免获取GIL的需要,我的兴趣在于探索在并行工作线程内分配多维数组的方法。 - ali_m
试一下并回报结果! - Veedrac
1
cpython.array已经在http://docs.cython.org/src/tutorial/array.html中有所描述。 代码应该被更改以包括“arr.data.as_doubles[i]”技巧,用于“原始C类型”基准测试,因为如果没有这个技巧,索引绝对不是原始的(当前的索引可以称为“普通cpython.array”索引,但它不是一个有趣的数据点)。 - Andreas
3
已经过去了几年,这仍然是一个非常好的答案/帖子。但我很失望,在cython中没有真正干净的解决方案来分配数组。在我的使用情况下,我必须在高级别上使用numpy数组,在低级别上使用malloc。在中间使用cpython数组似乎应该有更好的解决方案。我希望cython和/或numpy开发人员在未来能想出更好的解决方案。 - oli
显示剩余3条评论

9
作为对Veedrac答案的跟进:请注意,目前在Python 2.7中使用cpython.arraymemoryview支持似乎会导致内存泄漏。这似乎是一个长期存在的问题,因为它在cython-users邮件列表此处在2012年11月的帖子中提到。使用Cython版本0.22和Python 2.7.6或Python 2.7.9运行Veedrac的基准测试脚本会导致初始化cpython.array时使用buffermemoryview接口的大量内存泄漏。在Python 3.4上运行脚本时不会发生内存泄漏。我已经向Cython开发人员邮件列表提交了错误报告。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接