【问题标题】:2D MemoryView from dynamic arrays in CythonCython 中动态数组的 2D MemoryView
【发布时间】:2019-10-30 11:09:14
【问题描述】:

我知道这个question,但我一直在寻找一种更简单的方法来从 C 数组生成 2d 内存视图。由于我是 C 和 Cython 菜鸟,有人可以解释一下为什么像

cdef int[:, :] get_zeros(int d):
    # get 2-row array of zeros with d as second dimension
    cdef int i
    cdef int *arr = <int *> malloc(sizeof(int) * d)
    for i in range(d):
        arr[i] = 0
    cdef int[:, :] arr_view
    arr_view[0, :] = <int[:d]>arr
    arr_view[1, :] = <int[:d]>arr
    return arr_view

行不通?

编译时我得到Cannot assign type 'int[::1]' to 'int' 作为错误。这是否意味着 2d memview 被第一个 assign 语句折叠到 1d 还是因为 memoryviews 需要连续块等?

【问题讨论】:

    标签: python memory memory-management cython memoryview


    【解决方案1】:

    显然很难“解释为什么某些东西 [...] 行不通”,因为最终这只是一个设计决策,本可以采取不同的方式。但是:

    Cython 内存视图被设计为非常愚蠢。他们所做的只是提供一些很好的语法来访问实现 the Python buffer protocol 的东西的内存,然后提供一点额外的语法来让你做一些事情,比如获取指针的 1D 内存视图。

    此外,内存视图作为一个整体包装了一些东西。当您创建 cdef int[:, :] arr_view 时,它在您创建 arr_view = something 之前是无效的。尝试分配它的一部分是无稽之谈,因为(a)它会将分配委托给它使用缓冲区协议包装的东西,并且(b)分配的确切工作方式取决于您包装的缓冲区协议的格式.如果包装“间接”缓冲区协议对象,您所做的可能是有效的,但如果包装连续数组则没有任何意义。由于arr_view 可能会包装,因此 Cython 编译器必须将其视为错误。

    The question you link to 实现了缓冲区协议,因此是实现这种数组的正确方法。您正在尝试做的是采用额外的语法,从指针中提供 1D 内存视图,并将其强制转换为 2D 内存视图的一部分,模糊地希望这可能会起作用。这需要大量的逻辑,远远超出 Cython 内存视图的设计范围。


    可能还有几点值得补充:

    • 指针的内存视图不处理指针的释放(因为它们几乎不可能事后猜测你想要什么)。你必须处理这个逻辑。如果有效,您当前的设计会泄漏内存。在您链接到包装类的设计中,可以在__dealloc__ 中实现此功能(尽管该答案中未显示),因此效果更好。

    • 我个人的观点是“参差不齐的数组”(指向指针的二维数组)很糟糕。它们需要大量的分配和释放。有很多机会对它们进行半初始化。访问它们需要几个间接级别,因此速度很慢。唯一对他们有用的是它们在 C 中提供了 arr[idx1][idx2] 语法。总的来说,我更喜欢 Numpy 分配一维数组并使用形状/步幅来确定索引位置的方法。 (显然,如果您要包装现有库,那么您可能不是您的选择......)

    【讨论】:

    • 感谢您的详细解答!我已经想过,我在那里努力实现的目标就像是在作弊。就像你的建议一样,我最终为我的问题实现了一个一维数组。
    【解决方案2】:

    除了@DavidW 提供的精彩答案之外,我还想补充一些信息。在您包含的代码中,我看到您正在分配一个整数数组,然后在 for 循环中将内容清零。实现这一点的更方便的方法是改用 C 的 calloc 函数,它保证了指向零内存的指针,并且之后不需要 for 循环。

    此外,您可以创建一个指向数据“数组”的单个 int *,该数组的总大小为 2 * d * sizeof(int)。这将确保数据的两个“行”彼此相邻,而不是分开且参差不齐。然后可以将其直接转换为 2d 内存视图。

    正如 cmets 中所承诺的,转换代码如下所示(包括使用 calloc):

    cdef int[:, :] get_zeros(int d):    
        cdef int *arr = <int *>calloc(2 * d, sizeof(int))
        cdef int[:, :] arr_view = <int[:2, :d]>arr
        return arr_view
    

    如果您想尝试一下,根据文档,python c-api 中似乎也有一个calloc equivalent。但是,it does not appear to be wrapped 在 cython 的 mem.pxd 模块中,这就是您可能无法找到它的原因。您可以在代码中声明一个类似的 extern 块,以像该链接中包含的其他函数一样包装它。

    这里是bonus link,如果您想了解更多关于编写分配器以在走预分配路线时从大块中分配内存(即 PyMem_* 函数可能在幕后执行的操作,但更多可针对您的特定用例进行调整并由您控制)。

    【讨论】:

    • 感谢您的意见!我已经考虑过calloc,但是Cython documentation 建议从cpython.mem 导入用于管理内存的C-API 函数,我还没有找到C 的calloc 的等效函数。此外,我使用PyMem_Malloc as malloc 和for 循环与libc.stdlib 中的calloc 进行了基准测试,几乎没有区别。关于你的第二点,我不确定我将如何实施这一点。你能提供一个小代码sn-p或例子吗?
    • @zeawoas 有兴趣了解性能比较。几个小时后我回家后,我应该能够为第二点写一个 sn-p,因为我现在远离电脑。基本上,您应该能够使用像 cdef int[:,:] arr_view = arr 这样的方括号进行转换。
    • 就我个人而言,我强烈倾向于为您的二维数组使用 Numpy 数组,而不是 malloc 的某些变体。除非你有充分的理由不...
    • @CodeSurgeon 我刚刚重新运行了基准测试,libc.stdlib.malloc + for 循环和libc.stdlib.calloc 的速度几乎相同,而 cpython.mem.PyMem_Malloc + for 循环对于小型阵列 (100x100) 更快,对于更大的数组(10000x10000),这似乎与前面引用的 Cython 文档一致。 @DavidW 我正在调用一个递归函数,它必须生成一个小数组数百万次,并且切换到 malloc 为我节省了大约 50% 的运行时间。否则我也更喜欢 numpy。
    • 这对我来说似乎是合理的。很可能 python 进行了一些批处理,以最大限度地减少对 malloc 的调用次数,其内存管理功能用于文档状态的小分配。对 malloc 的每次调用都需要在“用户”空间和“内核”空间中进行处理,而批处理可以避免这种情况。您可以查看this video,它从 gamdev 的角度描述了这些成本。
    猜你喜欢
    • 2019-05-23
    • 2019-05-18
    • 1970-01-01
    • 2016-09-07
    • 1970-01-01
    • 2021-05-13
    • 2019-06-21
    • 2012-09-29
    • 1970-01-01
    相关资源
    最近更新 更多