强制NumPy ndarray在Cython中获取其内存的所有权

kyn*_*nan 13 python arrays numpy cython

在接下来的回答"我可以强迫一个numpy ndarray获取其记忆的所有权吗?" 我尝试PyArray_ENABLEFLAGS通过Cython的NumPy包装器使用Python C API函数,发现它没有暴露.

以下尝试手动公开它(这只是重现故障的最小示例)

from libc.stdlib cimport malloc
import numpy as np
cimport numpy as np

np.import_array()

ctypedef np.int32_t DTYPE_t

cdef extern from "numpy/ndarraytypes.h":
    void PyArray_ENABLEFLAGS(np.PyArrayObject *arr, int flags)

def test():
    cdef int N = 1000

    cdef DTYPE_t *data = <DTYPE_t *>malloc(N * sizeof(DTYPE_t))
    cdef np.ndarray[DTYPE_t, ndim=1] arr = np.PyArray_SimpleNewFromData(1, &N, np.NPY_INT32, data)
    PyArray_ENABLEFLAGS(arr, np.NPY_ARRAY_OWNDATA)
Run Code Online (Sandbox Code Playgroud)

失败并出现编译错误:

Error compiling Cython file:
------------------------------------------------------------
...
def test():
    cdef int N = 1000

    cdef DTYPE_t *data = <DTYPE_t *>malloc(N * sizeof(DTYPE_t))
    cdef np.ndarray[DTYPE_t, ndim=1] arr = np.PyArray_SimpleNewFromData(1, &N, np.NPY_INT32, data)
    PyArray_ENABLEFLAGS(arr, np.NPY_ARRAY_OWNDATA)
                          ^
------------------------------------------------------------

/tmp/test.pyx:19:27: Cannot convert Python object to 'PyArrayObject *'
Run Code Online (Sandbox Code Playgroud)

我的问题:在这种情况下,这是正确的方法吗?如果是这样,我做错了什么?如果没有,我如何强制NumPy在Cython中获得所有权,而不需要转到C扩展模块?

Ste*_*fan 17

您只是在接口定义中有一些小错误.以下对我有用:

from libc.stdlib cimport malloc
import numpy as np
cimport numpy as np

np.import_array()

ctypedef np.int32_t DTYPE_t

cdef extern from "numpy/arrayobject.h":
    void PyArray_ENABLEFLAGS(np.ndarray arr, int flags)

cdef data_to_numpy_array_with_spec(void * ptr, np.npy_intp N, int t):
    cdef np.ndarray[DTYPE_t, ndim=1] arr = np.PyArray_SimpleNewFromData(1, &N, t, ptr)
    PyArray_ENABLEFLAGS(arr, np.NPY_OWNDATA)
    return arr

def test():
    N = 1000

    cdef DTYPE_t *data = <DTYPE_t *>malloc(N * sizeof(DTYPE_t))
    arr = data_to_numpy_array_with_spec(data, N, np.NPY_INT32)
    return arr
Run Code Online (Sandbox Code Playgroud)

这是我的setup.py档案:

from distutils.core import setup, Extension
from Cython.Distutils import build_ext
ext_modules = [Extension("_owndata", ["owndata.pyx"])]
setup(cmdclass={'build_ext': build_ext}, ext_modules=ext_modules)
Run Code Online (Sandbox Code Playgroud)

python setup.py build_ext --inplace.构建.然后验证数据是否实际拥有:

import _owndata
arr = _owndata.test()
print arr.flags
Run Code Online (Sandbox Code Playgroud)

其中,你应该看到OWNDATA : True.

而且,这绝对是处理这个正确的方式,因为numpy.pxd究竟到所有其他功能导出到用Cython同样的事情.


ead*_*ead 7

@Stefan的解决方案适用于大多数情况,但有点脆弱。NumpyPyDataMem_NEW/PyDataMem_FREE用于内存管理,它是一个实现细节,这些调用被映射到通常的malloc/free+ 一些内存跟踪(我不知道 Stefan 的解决方案对内存跟踪有什么影响,至少它似乎不会崩溃)。

还有更多深奥的情况可能,其中freenumpy-library 不使用与malloccython 代码相同的内存分配器(链接到不同的运行时,例如在此github-issue或此SO-post 中)。

传递/管理数据所有权的正确工具是PyArray_SetBaseObject.

首先我们需要一个python对象,它负责释放内存。我在这里使用自制的 cdef 类(主要是因为日志记录/演示),但显然还有其他可能性:

%%cython
from libc.stdlib cimport free

cdef class MemoryNanny:
    cdef void* ptr # set to NULL by "constructor"
    def __dealloc__(self):
        print("freeing ptr=", <unsigned long long>(self.ptr)) #just for debugging
        free(self.ptr)
        
    @staticmethod
    cdef create(void* ptr):
        cdef MemoryNanny result = MemoryNanny()
        result.ptr = ptr
        print("nanny for ptr=", <unsigned long long>(result.ptr)) #just for debugging
        return result

 ...
Run Code Online (Sandbox Code Playgroud)

现在,我们使用MemoryNanny-object 作为内存的哨兵,一旦 parent-numpy-array 被破坏,它就会被释放。代码有点笨拙,因为PyArray_SetBaseObject窃取了引用,这不是 Cython 自动处理的:

%%cython
...
from cpython.object cimport PyObject
from cpython.ref cimport Py_INCREF

cimport numpy as np

#needed to initialize PyArray_API in order to be able to use it
np.import_array()


cdef extern from "numpy/arrayobject.h":
    # a little bit awkward: the reference to obj will be stolen
    # using PyObject*  to signal that Cython cannot handle it automatically
    int PyArray_SetBaseObject(np.ndarray arr, PyObject *obj) except -1 # -1 means there was an error
          
cdef array_from_ptr(void * ptr, np.npy_intp N, int np_type):
    cdef np.ndarray arr = np.PyArray_SimpleNewFromData(1, &N, np_type, ptr)
    nanny = MemoryNanny.create(ptr)
    Py_INCREF(nanny) # a reference will get stolen, so prepare nanny
    PyArray_SetBaseObject(arr, <PyObject*>nanny) 
    return arr
...
Run Code Online (Sandbox Code Playgroud)

这是一个示例,说明如何调用此功能:

%%cython
...
from libc.stdlib cimport malloc
def create():
    cdef double *ptr=<double*>malloc(sizeof(double)*8);
    ptr[0]=42.0
    return array_from_ptr(ptr, 8, np.NPY_FLOAT64)
Run Code Online (Sandbox Code Playgroud)

可以按如下方式使用:

>>> m =  create()
nanny for ptr= 94339864945184
>>> m.flags
...
OWNDATA : False
...
>>> m[0]
42.0
>>> del m
freeing ptr= 94339864945184
Run Code Online (Sandbox Code Playgroud)

与预期的结果/输出。

注意:结果数组并不真正拥有数据(即标志 return OWNDATA : False),因为内存是由内存保姆拥有的,但结果是相同的:一旦删除数组,内存就会被释放(因为没有人持有不再提及保姆)。


MemoryNanny不必保护原始 C 指针。它可以是其他任何东西,例如也可以是std::vector

%%cython -+
from libcpp.vector cimport vector
cdef class VectorNanny:
    #automatically default initialized/destructed by Cython:
    cdef vector[double] vec 
    @staticmethod
    cdef create(vector[double]& vec):
        cdef VectorNanny result = VectorNanny()
        result.vec.swap(vec) # swap and not copy
        return result
   
# for testing:
def create_vector(int N):
    cdef vector[double] vec;
    vec.resize(N, 2.0)
    return VectorNanny.create(vec)
Run Code Online (Sandbox Code Playgroud)

以下测试表明,保姆工作:

nanny=create_vector(10**8) # top shows additional 800MB memory are used
del nanny                  # top shows, this additional memory is no longer used.
Run Code Online (Sandbox Code Playgroud)


Mik*_*Lui 5

最新的 Cython 版本允许您使用最少的语法,尽管开销比建议的低级解决方案略多。

numpy_array = np.asarray(<np.int32_t[:10, :10]> my_pointer)

https://cython.readthedocs.io/en/latest/src/userguide/memoryviews.html#coercion-to-numpy

这本身并不能传递所有权。

值得注意的是,这个调用生成了一个 Cython 数组,通过array_cwrapper.

这会生成cython.array, 而不分配内存。的cython.array用途stdlib.h mallocfree在默认情况下,所以可以预计,使用默认的malloc,为好,而不是任何特殊的CPython / NumPy的分配器。

free仅在为此设置所有权时才调用cython.array,默认情况下仅在分配数据时才调用。对于我们的情况,我们可以通过以下方式手动设置:

my_cyarr.free_data = True


所以要返回一个一维数组,它会很简单:

from cython.view cimport array as cvarray

# ...
    cdef cvarray cvarr = <np.int32_t[:N]> data
    cvarr.free_data = True
    return np.asarray(cvarr)
Run Code Online (Sandbox Code Playgroud)