基于类型化 memoryview 让 Numpy 数组和 C 数组共享内存

简介: 基于类型化 memoryview 让 Numpy 数组和 C 数组共享内存


view C 级数组




Cython 的类型化 memoryview 还可以 view 一个 C 级数组,并且数组可以是在堆上分配的,也可以是在栈上分配的。如果要 view 一个栈上分配的 C 数组,那么直接将该数组赋值即可,因为数组的大小是固定的(或完整的),Cython 有足够的信息来跟踪这个 C 数组。

import numpy as np
# 声明一个 C 数组
cdef int a[3][5][7]
# 类型化 memoryview 是 C 连续的
# 因为 C 里面的数组是 C 连续的
cdef int[:, :, :: 1] m = a
# 然后将其赋值为 123
m[...] = 123
# 转成 Numpy 中的数组
arr = np.array(m, copy=False)
print(np.sum(arr), 3 * 5 * 7 * 123)
"""
12915 12915
"""
print(arr[:, 1: 3])
"""
[[[123 123 123 123 123 123 123]
  [123 123 123 123 123 123 123]]
 [[123 123 123 123 123 123 123]
  [123 123 123 123 123 123 123]]
 [[123 123 123 123 123 123 123]
  [123 123 123 123 123 123 123]]]
"""

上面的 C 数组是在栈区分配的,在赋值给类型化 memoryview 的时候,等号右边写一个数组名即可,因为 Cython 清楚 C 数组的形状。当然我们也可以改为堆区分配:

from libc.stdlib cimport malloc, free
import numpy as np
cdef int *a = <int *>malloc(3 * 5 * 7 * sizeof(int))
# 很明显, 改成堆区分配的话, 形状的信息就丢失了
# Cython 只知道这个一个 int *,如果将 a 赋值给类型化 memoryview
# 编译时会出现 "Cannot convert long * to memoryviewslice"
# 因此我们在赋值给类型化 memoryview 的时候, 必须给 Cython 提供更多的信息
# 而 <int[:3, :5, :7]> a 会告诉 Cython 这是一个三维数组, 维度分别是 3 5 7
# 当然我们这里变成 7 5 3 也是可以的, 因为形状是由我们决定的
cdef int[:, :, :: 1] m = <int[:3, :5, :7]> a
m[...] = 123
# 转成 Numpy 中的数组
arr = np.array(m, copy=False)
print(np.sum(arr), 3 * 5 * 7 * 123)
"""
12915 12915
"""
print(arr[:, 1: 3])
"""
[[[123 123 123 123 123 123 123]
  [123 123 123 123 123 123 123]]
 [[123 123 123 123 123 123 123]
  [123 123 123 123 123 123 123]]
 [[123 123 123 123 123 123 123]
  [123 123 123 123 123 123 123]]]
"""

在 C 级别,光靠一个头指针没有办法确定动态分配的 C 数组的形状,而这一点则需要由我们来确定。因此将一个 C 数组赋值给类型化 memoryview 时,如果数据不正确,那么可能会导致缓冲区溢出、段错误,或者数据损坏等等。

到此我们就算介绍完了类型化 memoryview 的特性,并展示了如何在支持缓冲区协议的 Python 对象和 C 级数组中使用它。如果 Cython 函数中有一个类型化 memoryview 参数,那么可以传递一个支持缓冲区协议的 Python 对象或者 C 数组作为参数进行调用。

然后是返回值,Python 中有一个 memoryview,而 Cython 在 memoryview 的基础上构建了类型化 memoryview。当在函数中想返回一个类型化 memoryview 时,Cython 会根据缓冲区内容(没有拷贝)构建一个 Python 的 memoryview 返回。

但这会有一些问题,假设在函数中返回一个由 C 数组构建的类型化 memoryview,如果这个 C 数组是在堆区通过 malloc 动态分配的,那么返回没有任何问题(表面上);但如果它是在栈区分配的,在函数结束后就会被销毁,而我们说 Cython 又不会对缓冲区内容进行拷贝,因此会出现错误。

所以如果想返回 C 数组给其它函数使用,那么需要在堆区分配。但即便 C 数组在堆区分配,也是存在问题的。那就是当我们不再使用 memoryview 的时候,谁来释放这个在堆上申请的数组呢?如何正确地管理它的内存呢?不过在探讨这个问题之前,我们需要先来说一下另一种 Numpy。


另一种 Numpy




为啥会有另一种 Numpy 呢?因为我们在 Cython 中除了 import numpy 之外,还可以 cimport numpy

在类型化 memoryview 出现之前,Cython 也可以使用不同的语法来很好地处理 Numpy 数组,这便是原始缓冲区语法。尽管它已经被类型化 memoryview 所取代,但我们依旧可以正常使用它。

# 文件名:cython_test.pyx
# 这里一定要使用 cimport numpy
# 或者 from numpy cimport ndarray
cimport numpy as np 
# 如果是 import numpy as np
# 那么不好意思, np.ndarray 是无法作为参数类型和返回值类型的
# 编译时会报错: 'np' is not a cimported module
# 如果是 from numpy import ndarray
# 编译时同样报错: 'ndarray' is not a type identifier
cpdef np.ndarray func(np.ndarray array):
    return array

但是注意:此时不能自动编译,因为它依赖 numpy 的一个头文件,所以我们需要通过手动编译的方式。

from pathlib import Path
import numpy as np
from distutils.core import Extension, setup
from Cython.Build import cythonize
ext = Extension(
    "cython_test",
    ["cython_test.pyx"],
    # cimport numpy 时会使用 Numpy 提供的一个头文件:arrayobject.h
    # 但是很明显, 我们并没有指定它的位置
    # 因此需要通过 include_dirs 告诉 Cython 编译器去哪里找这个头文件
    # 如果没有这个参数, 那么编译时会报错:
    # fatal error: numpy/arrayobject.h: No such file or directory
    include_dirs=[str(Path(np.__file__).parent / "core" / "include")])
# 当然 numpy 也为我们封装了一个函数, 直接通过 np.get_include() 即可获取该路径
# 对于我当前的环境来说就是 C:\python38\lib\site-packages\numpy\core\include
setup(
    ext_modules=cythonize(ext, language_level=3),
)

编译之后导入测试一下:

import numpy as np
from cython_test import func
print(func(np.array([[1, 2], [3, 4]])))
"""
[[1 2]
 [3 4]]
"""
print(
    func(np.array([["xx", None], [(1, 2), {1, 2}]], dtype="O"))
)
"""
[['xx' None]
 [(1, 2) {1, 2}]]
"""

测试是没有问题的,接收的是 array,返回的也是 array。并且我们看到,这对 array 的类型没有任何限制,但如果我们希望限制 array 的类型、甚至是维度,这个时候该怎么做呢?

Cython 为 numpy 提供了专门的方法,比如希望接收一个元素类型为 int64、维度为 2 的数组,就可以使用 ndarray[long, ndim=2] 这种方式,我们演示一下。

cimport numpy as np
# C 类型和 Numpy 的类型要统一
# long 对应 np.int64, int 对应 np.int32
# short 对应 np.int16, char 对应 np.int8
# unsigned long 对应 np.uint64, 其它同理
def func1(np.ndarray[long, ndim=2] array):
    print(array)
def func2(np.ndarray[double, ndim=1] array):
    print(array)
def func3(np.ndarray[object, ndim=1] array):
    print(array)
    
# 除了作为函数参数和返回值类型之外, 还可以用来声明普通的静态变量
# 比如: cdef np.ndarray[double, ndim=2] arr

编译之后导入测试:

import numpy as np
import cython_test
# 这里我们传递的时候, 参数和维度一定要匹配
# Numpy 的整型默认是 int64
cython_test.func1(np.array([[1, 2], [3, 4]]))
"""
[[1 2]
 [3 4]]
"""
try:
    cython_test.func1(np.array([1, 2, 3, 4]))
except ValueError as e:
    print(e)
"""
Buffer has wrong number of dimensions (expected 2, got 1)
"""
try:
    cython_test.func2(np.array([1, 2, 3, 4]))
except ValueError as e:
    print(e)
"""
Buffer dtype mismatch, expected 'double' but got 'long'
"""
cython_test.func2(np.array([1, 2, 3, 4], dtype="float64"))
"""
[1. 2. 3. 4.]
"""
cython_test.func3(np.array(["a", "b", object], dtype="O"))
"""
['a' 'b' <class 'object'>]
"""

以上就是原始缓冲区语法,现在更推荐类型化 memoryview,虽然它比 Numpy 中的 array 少了许多功能,但我们说这两者之间是可以高效转换的。并且如果是通过 np 来调用的话,那么两者是等价的。举个例子:

import numpy as np
def func(int[:, :: 1] m):
    # m 本身没有 sum 方法
    # 但是我们可以将它传递给 np.sum
    print(np.sum(m))
    print(np.sum(m, axis=0))
    print(np.sum(m, axis=1))
func(np.array([[1, 2], [3, 4]], dtype="int32"))
"""
10
[4 6]
[3 7]
"""

因此这种声明方式是更加推荐的,而且也更加清晰和简洁,以及我们可以使用 pyximport 自动编译了。之前的方式由于依赖一个头文件,必须要手动编译,并告诉 Cython 编译器头文件去哪里找。但是现在不需要了,因为我们根本没有 cimport numpy

但是以上这些说实话都不能算是优点,所以肯定还有其它的优点,那么都有哪些呢?

1)类型化 memoryview 支持的对象种类非常多,只要它们实现了缓冲区协议,比如:Numpy array, bytes 对象等等,并且它也适用于 C 数组。所以它比原始缓冲区语法更加通用,原始缓冲区语法只适用于 Numpy array。

2)类型化 memoryview 有着更多的选择来控制数组的特性,比如是 C 连续还是 Fortran 连续,是直接访问还是间接访问。并且一些选项可以按照维度逐个控制,而 Numpy 的原始缓冲区语法不提供这种级别的控制。

3)在任何情况下,类型化 memoryview 都有着超越原始缓冲区语法的性能,这一点才是我们最关注的。


包装 C 数组




回到之前的问题,当 C 数组在堆上分配,那么返回之后要如何释放堆区申请的内存呢?我们举个例子:

// heap_malloc.h
float *make_matrix_c(int nrows, int ncols);
// heap_malloc.c
#include <stdlib.h>
float *make_matrix_c(int nrows, int ncols) {
    float *matrix = (float *) malloc(nrows * ncols * sizeof(float));
    return matrix;
}

以上返回一个在堆上分配的 C 数组:

cdef extern from "heap_malloc.h":
    float *make_matrix_c(int nrows, int ncols)
def make_matrix(int nrows, int ncols):
    cdef float *arr = make_matrix_c(nrows, ncols)
    cdef float[:, :: 1] m = <float[:nrows, :ncols]> arr
    # 因为元素都未初始化, 所以里面的值都是不确定的
    # 虽然这无伤大雅, 但是更优雅的处理方式是将值都初始化为零值
    m[...] = 0.0
    # m 是类型化 memoryview,返回之后会转成 Python 的 memoryview
    return m

显然这个例子已经无需解释了,我们直接编译:

from distutils.core import Extension, setup
from Cython.Build import cythonize
ext = Extension("cython_test",
                ["cython_test.pyx", "heap_malloc.c"],
                include_dirs=["."])
setup(
    ext_modules=cythonize(ext, language_level=3),
)

编译完成之后将 pyd 文件移到当前目录,导入测试:

import numpy as np
import cython_test
m = cython_test.make_matrix(3, 4)
# 转成 memoryview 返回
print(m)
"""
<MemoryView of 'array' object>
"""
print(m.shape) 
"""
(3, 4)
"""
# 基于 m 创建 numpy 数组
# np.asarray 等价于 np.array,但是它不会拷贝缓冲区
# 而 np.array 默认会拷贝缓冲区
# 当然也可以通过指定 copy=False,让其不拷贝
arr1 = np.asarray(m)
arr2 = np.asarray(m)
# arr1 和 arr2 都是基于 m 创建的
# 它们使用的都是 m 的缓冲区
# 也就是 heap_malloc.c 里面的 make_matrix_c 函数返回的 C 数组
print(arr1)
"""
[[0. 0. 0. 0.]
 [0. 0. 0. 0.]
 [0. 0. 0. 0.]]
"""
print(arr2)
"""
[[0. 0. 0. 0.]
 [0. 0. 0. 0.]
 [0. 0. 0. 0.]]
"""
# 修改 arr1,也会影响 arr2
# 因为它们共享同一个缓冲区
arr1[1, 1] = 123
print(arr2)
"""
[[  0.   0.   0.   0.]
 [  0. 123.   0.   0.]
 [  0.   0.   0.   0.]]
"""

我们实现的函数 make_matrix 的功能就是初始化一个元素全为 0、行和列分别为 nrows 和 ncols 的数组,然后根据这个数组创建一个 memoryview 并返回。从打印结果来看,代码没有任何问题,很 happy,但事实真是如此吗?

明显不是,因为 arr1 和 arr2 对应的 C 数组是在堆上申请的,那么这个堆区的 C 数组咋办?所以目前这个 make_matrix 函数是存在致命缺陷的,它有内存泄露的风险,而这个风险对于任何一个程序而言都是致命的。


使用 Cython 正确(自动)地管理 C 数组




作为开发者,我们需要对内存负责,但当和 C 共享数组的时候,合适的解决内存问题就变成了一件很棘手的事情,因为 C 语言没有自动管理内存的特性。通常在这种情况下,最干净利索的做法就是复制数据,来澄清各自对数据的所有权。

比如我们可以不返回 memoryview,而是直接创建一个 Numpy 数组返回,并且不使用 copy=False,这样就会将缓冲区拷贝一份。所以结果就是:你的是你的,我的是我的,两者之间没有关系。

from libc.stdlib cimport free
import numpy as np
cdef extern from "heap_malloc.h":
    float *make_matrix_c(int nrows, int ncols)
def make_matrix(int nrows, int ncols):
    cdef float *arr = make_matrix_c(nrows, ncols)
    cdef float[:, :: 1] m = <float[:nrows, :ncols]> arr
    m[...] = 0.0
    # 不加 copy=False,会创建新的缓冲区
    result = np.array(m)
    # 释放掉 C 数组 arr
    free(<void *> arr)
    return result

通过将缓冲区拷贝一份,这样就可以放心的释放 C 数组了,不会出现内存泄露。但很明显,如果数据量非常大,我们这么做是不是会影响效率呢?所以这虽然是一个解决问题的办法,但不是最好的办法。

而最好的办法还是共享内存,不要让 Numpy 新建一个缓冲区,而是使用已有的 C 数组,避免内存的拷贝。但这又回到了之前的问题,如果 Python 后续不再使用,那对应的 C 数组应该怎么释放呢?

先来介绍第一种方法:

from libc.stdlib cimport free
import numpy as np
# 定义一个全局变量
cdef void *release_pointer = NULL
cdef extern from "heap_malloc.h":
    float *make_matrix_c(int nrows, int ncols)
def make_matrix(int nrows, int ncols):
    cdef float *arr = make_matrix_c(nrows, ncols)
    # 将指针 arr 赋值给全局变量 release_pointer
    global release_pointer
    release_pointer = <void *> arr
    cdef float[:, :: 1] m = <float[:nrows, :ncols]> arr
    m[...] = 0.0
    # 可以返回 memoryview,也可以返回一个 array
    # 这里直接返回 array,并且不拷贝缓冲区
    return np.asarray(m)
def dealloc():
    if release_pointer != NULL:
        free(release_pointer)

我们在创建 C 数组的时候,将指针用全局变量保存起来,这样 Python 就可以调用 dealloc 函数释放了。

重新编译,然后导入:

import cython_test
while True:
    # 会在堆区创建一个 C 数组
    cython_test.make_matrix(3, 4)
    # 调用 dealloc 函数将 C 数组释放掉
    # 如果没有这一步,内存占用会不断往上涨
    cython_test.dealloc()

写了一个死循环,每一次循环都会在堆区申请一个 C 数组,如果不调用 dealloc,那么内存占用会蹭蹭往上涨。但是我们调用了 dealloc,每次不用了就会释放掉,所以内存不会出现泄露。

上面这种做法虽然能解决问题,但仍存在两个缺陷。第一个缺陷是实现方式比较 low,因为每次不用了,还需要手动调用 dealloc 函数进行释放;而第二个缺陷就比较严重了,当第一次调用 make_matrix 函数时,会在堆区分配一个 C 数组,然后全局变量保存该数组的地址。如果再调用一次 make_matrix 函数,那么又会在堆区分配一个 C 数组,然后全局变量会保存这个新数组的地址。那么问题来了,第一次在堆上申请的 C 数组怎么办?

如果文字不好理解的话,我们用代码来解释:

# arr1 会对应一个堆区的 C 数组
# 全局变量 release_pointer 保存该数组的地址
arr1 = cython_test.make_matrix(6, 6)
# arr2 也会对应一个堆区的 C 数组
# 全局变量又会保存新的 C 数组的地址
arr2 = cython_test.make_matrix(5, 10)
# 此时调用 dealloc 释放的是 arr2 对应的 C 数组
# 那么 arr1 对应的 C 数组咋办?
cython_test.dealloc()

相信你应该发现问题所在了,因为全局变量 release_pointer 每次只能保存一个地址,所以在调用完 make_matrix 之后,必须先调用 dealloc 将已有的 C 数组给释放掉,然后才能再一次调用 make_matrix。

比如上面的 arr1,在创建 arr2 之前必须先把 arr1 对应的 C 数组释放掉,否则的话,全局变量就会保存 arr2 对应的 C 数组的地址。那么 arr1 对应的 C 数组,就永远也没有机会释放了。

所以这种做法的局限性比较高(但是方便),而优雅的做法应该是把 C 数组和返回的 Numpy 数组关联起来,一旦当 Numpy 数组被回收,那么就自动释放堆区的 C 数组。

cimport numpy as c_np
import numpy as np
cdef extern from "heap_malloc.h":
    float *make_matrix_c(int nrows, int ncols)
cdef extern from "numpy/ndarraytypes.h":
    # 需要使用 Numpy 提供的 C API
    # 另外 Numpy 的数组在底层对应的结构体是 PyArrayObject
    void PyArray_ENABLEFLAGS(c_np.PyArrayObject *arr, int flags)
def make_matrix(int nrows, int ncols):
    cdef float *arr = make_matrix_c(nrows, ncols)
    cdef float[:, ::1] m = <float [:nrows, :ncols]>arr
    m[...] = 0
    # 转成 Numpy 的数组,并且和 C 数组共享内存
    result = np.asarray(m)
    # 对于 Numpy 数组而言,缓冲区有两种选择方式
    # 可以使用已有的缓冲区,也可以新创建一个缓冲区
    # 如果是新创建的缓冲区,那么该缓冲区就归属于对应的 Numpy 数组
    # 当 Numpy 数组被回收时,会顺带将缓冲区一块回收
    # 但如果使用已有的缓冲区,那么该缓冲区就不属于 Numpy 数组了
    # 比如当前的 result,它就是直接使用已有的 C 数组作为缓冲区
    # 那么当 Numpy 数组被回收时,缓冲区是不会被回收的,因为缓冲区不属于它(出现内存泄露)
    # 但我们通过下面这个函数将 Numpy数组的 flags设置为 NPY_ARRAY_OWNDATA
    # 相当于告诉 Numpy:缓冲区属于数组 result,如果它被回收了,
    #                 请在 __dealloc__ 里面将缓冲区(C 数组)也释放掉
    PyArray_ENABLEFLAGS(<c_np.PyArrayObject *>result,
                        c_np.NPY_ARRAY_OWNDATA)
    # 此时我们就解决了内存泄漏问题
    return result

再来描述一下背后的原理,实现了缓冲区协议的对象,数据都存在缓冲区里面。缓冲区是一个一维数组,由 Py_buffer 里面的 buf 成员指向。缓冲区可以是自己的,也就是对象在创建的时候,也会新建一个缓冲区;当然缓冲区也可以是别人的,就是对象在创建的时候,直接使用已有的缓冲区。

对于当前代码来说,里面的缓冲区就不属于数组 result,它属于 C 数组。因此 result 在被回收的时候,是不会管这个缓冲区的;但我们通过 PyArray_ENABLEFLAGS 将它的 flags 设置成了 NPY_ARRAY_OWNDATA,让它拥有了缓冲区的所有权。然后当 Numpy 数组被回收时,也会将缓冲区给回收掉,对于当前的例子而言,表现就是 Numpy 数组回收时,C 数组也被回收了(或者说内存被释放了),避免了内存泄漏。

编译测试一下:

import cython_test
arr = cython_test.make_matrix(3, 3)
print(arr.__class__)
"""
<class 'numpy.ndarray'>
"""
print(arr)
"""
[[0. 0. 0.]
 [0. 0. 0.]
 [0. 0. 0.]]
"""
arr[1, 1] = 111
print(arr)
"""
[[  0.   0.   0.]
 [  0. 111.   0.]
 [  0.   0.   0.]]
"""

一切正常,并且此时不会出现内存泄漏。

基于缓冲区所有权引发的一些思考

我们上面的例子中,Numpy 数组在创建的时候直接使用了 C 数组作为自己的缓冲区,然后通过 PyArray_ENABLEFLAGS 让 Numpy 数组具有对缓冲区的所有权。这样在释放 Numpy 数组的时候,同时也会释放缓冲区(即 C 数组所占内存)。

所以看下面一段代码:

cimport numpy as c_np
import numpy as np
cdef extern from "numpy/ndarraytypes.h":
    void PyArray_ENABLEFLAGS(c_np.PyArrayObject *arr, int flags)
b = b"hello world"
arr = np.frombuffer(b, dtype="S1")
PyArray_ENABLEFLAGS(<c_np.PyArrayObject *>arr,
                    c_np.NPY_ARRAY_OWNDATA)
del arr
print(b)

你觉得上面代码在执行时会发生什么结果呢?很明显,解释器会异常崩溃。原因是数组 arr 使用的缓冲区不是它自己的,缓冲区是 bytes 对象的,但它获取了缓冲区的所有权。所以 del arr 之后,释放的不只是 Numpy 数组,还有它内部的缓冲区。

而 b 和 arr 共用一个缓冲区,并且 del arr 会将缓冲区也释放掉,那么再打印 b 会有什么后果呢?毫无疑问,解释器直接崩溃挂掉。因此当 Numpy 数组需要拥有所有权时,缓冲区基本都是来自堆区的 C 数组。

通过包装器实现堆区 C 数组的释放

上面释放 C 数组的方式可以说非常的优雅,但它使用了 Numpy 提供的 C API,而如果我们事先不知道这个 API 的话,那么能不能用其它的方式实现呢?

Linux 之父说过:一层架构搞不定,那就再套一层。我们这里的做法与之类似,只需要再定义一个包装器即可。

from libc.stdlib cimport free
cimport numpy as c_np
import numpy as np
cdef extern from "heap_malloc.h":
    float *make_matrix_c(int nrows, int ncols)
cdef class ArrayResult:
    # 堆区的 C 指针
    cdef void *_ptr
    # 转成 array 让外界访问
    cdef public c_np.ndarray array
    def __dealloc__(self):
        if self._ptr != NULL:
            free(self._ptr)
def make_matrix(int nrows, int ncols):
    cdef float *arr = make_matrix_c(nrows, ncols)
    cdef float[:, ::1] m = <float [:nrows, :ncols]>arr
    m[...] = 0
    # 创建一个 ArrayResult 实例对象充当包装器
    cdef ArrayResult art = ArrayResult()
    # 设置指针和数组
    art._ptr = <void *> arr
    art.array = np.asarray(m)
    # 然后将 ArrayResult 实例对象返回
    return art

编译测试一下:

import cython_test
art = cython_test.make_matrix(3, 3)
array = art.array
print(array)
"""
[[0. 0. 0.]
 [0. 0. 0.]
 [0. 0. 0.]]
"""
array[0, 1] = 111
print(array)
"""
[[  0. 111.   0.]
 [  0.   0.   0.]
 [  0.   0.   0.]]
"""
# 删除 art,会执行 __dealloc__ 方法
# 在内部会将堆区的 C 数组释放掉
del art
# 此时 array 就不能再用了
# 虽然这里打印没有报错,但内存已经被回收了
# 所以打印出来的就是乱七八糟的脏数据
print(array)
"""
[[-4.1069705e-16  7.1694633e-41  2.8628528e-42]
 [ 0.0000000e+00  0.0000000e+00  0.0000000e+00]
 [ 0.0000000e+00  0.0000000e+00  0.0000000e+00]]
"""

以上我们就实现了 Numpy 数组和 C 数组共享内存,当然这并不复杂,重点是 C 数组要如何释放?总共有三种方式:

1)每次创建 C 数组时,就用一个全局变量将其指针保存起来,然后再单独定义一个函数用于释放。但这种方式有一个很大的弊端,就是不能同时保存多个,在申请第二个 C 数组之前,必须先把第一个 C 数组释放掉。

2)让 Numpy 数组拥有缓冲区(C 数组)的所有权,这样在 Numpy 数组被回收时,C 数组也会被自动释放掉。该方式最优雅,但它需要用到 Numpy 提供的 C API,而你已经知道了这个 API,那么在工作中推荐使用第二种方式。

3)将 Numpy 数组和 C 数组封装起来,变成某个实例对象的两个属性,然后将 C 数组的释放逻辑写在 __dealloc__ 方法中。这样外界便可以通过该对象拿到想要的结果,并且也能保证 C 数组能够回收。

当然啦,最方便也是最稳妥的方式还是将数据拷贝一份。如果引入 C 只是为了快速计算,但返回的 C 数组不是很大,那么将数据拷贝一份也是个不错的选择。至于具体怎么做,就取决于你的业务需求。


小结




到目前为止,我们介绍了 Python 的各种可以转成类型化 memoryview 的对象,但是 Numpy array 绝对最具普遍性、灵活性以及表现力。而且除了 Python 对象之外,类型化 memoryview 还可以使用 C 级数组,不管是栈上分配,还是堆上分配。

Cython 的类型化 memoryview 的核心就在于,它提供了一个一致的抽象,这个抽象适用于所有支持缓冲区协议的对象。此外,它还为我们提供了高效的 C 级访问缓冲区,并且共享内存。

相关文章
|
25天前
|
存储 C语言 计算机视觉
在C语言中指针数组和数组指针在动态内存分配中的应用
在C语言中,指针数组和数组指针均可用于动态内存分配。指针数组是数组的每个元素都是指针,可用于指向多个动态分配的内存块;数组指针则指向一个数组,可动态分配和管理大型数据结构。两者结合使用,灵活高效地管理内存。
|
25天前
|
容器
在使用指针数组进行动态内存分配时,如何避免内存泄漏
在使用指针数组进行动态内存分配时,避免内存泄漏的关键在于确保每个分配的内存块都能被正确释放。具体做法包括:1. 分配后立即检查是否成功;2. 使用完成后及时释放内存;3. 避免重复释放同一内存地址;4. 尽量使用智能指针或容器类管理内存。
|
2月前
|
Rust 安全 Java
内存数组越界
【10月更文挑战第14天】
33 1
|
2月前
|
机器学习/深度学习 并行计算 大数据
【Python篇】NumPy完整指南(上篇):掌握数组、矩阵与高效计算的核心技巧2
【Python篇】NumPy完整指南(上篇):掌握数组、矩阵与高效计算的核心技巧
92 10
|
2月前
|
Python
Numpy学习笔记(四):如何将数组升维、降维和去重
本文介绍了如何使用NumPy库对数组进行升维、降维和去重操作。
53 1
|
2月前
|
Python
Numpy学习笔记(五):np.concatenate函数和np.append函数用于数组拼接
NumPy库中的`np.concatenate`和`np.append`函数,它们分别用于沿指定轴拼接多个数组以及在指定轴上追加数组元素。
45 0
Numpy学习笔记(五):np.concatenate函数和np.append函数用于数组拼接
|
2月前
|
Python
使用 NumPy 进行数组操作的示例
使用 NumPy 进行数组操作的示例
39 2
|
2月前
|
索引 Python
【Python篇】NumPy完整指南(上篇):掌握数组、矩阵与高效计算的核心技巧1
【Python篇】NumPy完整指南(上篇):掌握数组、矩阵与高效计算的核心技巧
114 4
|
2月前
|
机器学习/深度学习 并行计算 调度
CuPy:将 NumPy 数组调度到 GPU 上运行
CuPy:将 NumPy 数组调度到 GPU 上运行
108 1
|
3月前
|
Python
numpy | 插入不定长字符数组测试OK
本文介绍了如何在numpy中创建和操作不定长字符数组,包括插入和截断操作的测试。