【问题标题】:Passing list of numpy arrays to C using cython使用 cython 将 numpy 数组列表传递给 C
【发布时间】:2017-09-15 13:00:14
【问题描述】:

我有一个列表 list_of_arrays 的 3D numpy 数组,我想通过模板传递给 C 函数

int my_func_c(double **data, int **shape, int n_arrays)

这样

data[i]  : pointer to the numpy array values in list_of_arrays[i]
shape[i] : pointer to the shape of the array in list_of_arrays[i] e.g. [2,3,4]

如何使用 cython 接口函数调用my_func_c

我的第一个想法是做类似下面的事情(可行),但我觉得有一种更好的方法,只使用 numpy 数组而不进行分配和释放。

# my_func_c.pyx

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

cdef extern from "my_func.c":
    double my_func_c(double **data, int **shape, int n_arrays)

def my_func(list list_of_arrays):
    cdef int n_arrays  = len(list_of_arrays)
    cdef double **data = <double **> malloc(n_arrays*sizeof(double *))
    cdef int **shape   = <int **> malloc(n_arrays*sizeof(int *))
    cdef double x;

    cdef np.ndarray[double, ndim=3, mode="c"] temp

    for i in range(n_arrays):
        temp = list_of_arrays[i]
        data[i]  = &temp[0,0,0]
        shape[i] = <int *> malloc(3*sizeof(int))
        for j in range(3):
            shape[i][j] = list_of_arrays[i].shape[j]

    x = my_func_c(data, shape, n_arrays)

    # Free memory
    for i in range(n_arrays):
        free(shape[i])
    free(data)
    free(shape)

    return x

注意

要查看一个工作示例,我们可以使用一个非常简单的函数来计算列表中所有数组的乘积。

# my_func.c

double my_func_c(double **data, int **shape, int n_arrays) {
    int array_idx, i0, i1, i2;

    double prod = 1.0;

    // Loop over all arrays
    for (array_idx=0; array_idx<n_arrays; array_idx++) {
        for (i0=0; i0<shape[array_idx][0]; i0++) {
            for (i1=0; i1<shape[array_idx][1]; i1++) {
                for (i2=0; i2<shape[array_idx][2]; i2++) {
                    prod = prod*data[array_idx][i0*shape[array_idx][1]*shape[array_idx][2] + i1*shape[array_idx][2] + i2];
                }
            }
        }
    }

    return prod;
}

创建setup.py 文件,

# setup.py

from distutils.core import setup
from Cython.Build import cythonize
import numpy as np

setup(
    name='my_func',
    ext_modules = cythonize("my_func_c.pyx"),
    include_dirs=[np.get_include()]
    )

编译

python3 setup.py build_ext --inplace

最后我们可以运行一个简单的测试

# test.py

import numpy as np
from my_func_c import my_func

a = [1+np.random.rand(3,1,2), 1+np.random.rand(4,5,2), 1+np.random.rand(1,2,3)]

print('Numpy product: {}'.format(np.prod([i.prod() for i in a])))
print('my_func product: {}'.format(my_func(a)))

使用

python3 test.py

【问题讨论】:

  • 它怎么不起作用?
  • 好吧,我会写一个工作示例。
  • 什么是输出
  • 问题似乎不够清楚,但我不确定如何编辑它。这种情况下的输出本质上就是我们想要看到的。 cython 函数给出与 numpy 相同的结果(有一些小的舍入误差)。它的随机性每次都会改变。我想要的是一种在 cython 中处理 numpy 数组列表的方法,而无需求助于 mallocing 和释放内存。
  • @rwolst 您可以将指针传递给 numpy 的内置形状数组,而不是逐个元素地复制它们。除此之外,我认为您的方法非常好。

标签: python c numpy cython


【解决方案1】:

另一种选择是让 numpy 为您管理内存。您可以使用 np.uintp 的 numpy 数组来做到这一点,这是一个与任何指针大小相同的无符号整数。

不幸的是,这确实需要一些类型转换(在“指针大小的 int”和指针之间),这是隐藏逻辑错误的好方法,所以我对它不是 100% 满意。

def my_func(list list_of_arrays):
    cdef int n_arrays  = len(list_of_arrays)
    cdef np.uintp_t[::1] data = np.array((n_arrays,),dtype=np.uintp)
    cdef np.uintp_t[::1] shape = np.array((n_arrays,),dtype=np.uintp)
    cdef double x;

    cdef np.ndarray[double, ndim=3, mode="c"] temp

    for i in range(n_arrays):
        temp = list_of_arrays[i]
        data[i]  = <np.uintp_t>&temp[0,0,0]
        shape[i] = <np.uintp_t>&(temp.shape[0])

    x = my_func_c(<double**>(&data[0]), <np.intp_t**>&shape[0], n_arrays)

(我要指出的是,我只是确认它可以编译并没有进一步测试它,但基本思想应该是可以的)


您所做的方式可能是一种非常明智的方式。对应该可以工作的原始代码进行一点简化

shape[i] = <np.uintp_t>&(temp.shape[0])

而不是malloc 并复制。我还建议将frees 放在finally 块中以确保它们运行。


编辑: @ead 有帮助地指出 numpy shape is stored as as np.intp_t - 即一个大到足以容纳指针的有符号整数,主要是 64 位 - 而 int 通常是 32 位。因此,要在不复制的情况下传递形状,您需要更改 C api。投射帮助会使该错误更难发现(“隐藏逻辑错误的好方法”)

【讨论】:

  • @ead 关于使用 C++ 向量的建议也可能是一个合理的替代方案——它具有我的“numpy 数组”解决方案的一些优点,而且转换更少。不过,我会让他/她发布它...
  • 在原始解决方案中,还应该检查 malloc 是否成功,即结果不是 0。因此,如果不能选择 c++,我的权衡将是强制转换(您的解决方案)超过“我应该记住并做正确的许多事情”。
  • 实际上,反对强制转换的一点是:形状条目是 8 字节整数,但在强制转换之后,它们将被重新解释为 4 字节整数(至少在最常用的系统上)。
  • @ead 我猜这可能就是 OP 复制它们的原因。我对选角的问题主要在于它很容易犯这种错误。感谢您指出这一点
  • 所以你证明了自己是对的:) 我认为更安全的方法是首先重新解释保留的内存:cdef np.uintp_t[::1] shape_mem = np.array((n_arrays,),dtype=np.uintp); cdef int** shape=&lt;int**&gt;&amp;shape_mem[0],然后享受类型安全的优势:现在shape[i] = &amp;temp.shape[0] 会如果 temp.shape[0] 不是 int,则为编译错误。
【解决方案2】:

我认为这是从 C++ 代码中使用 C 功能的好模式,它也可以在这里使用,并且有两个优点:

  1. 内存管理得到照顾。
  2. 感谢模板,无需强制转换,因此我们仍然拥有 c 类型安全的安全网。

要解决您的问题,您可以使用std::vector:

import numpy as np
cimport numpy as np
from libcpp.vector cimport vector

cdef extern from "my_func.c":
    double my_func_c(double **data, int **shape, int n_arrays)

def my_func(list list_of_arrays):
    cdef int n_arrays  = len(list_of_arrays)
    cdef vector[double *] data
    cdef vector [vector[int]] shape_mem # for storing casted shapes
    cdef vector[int *] shape  #pointers to stored shapes
    cdef double x
    cdef np.ndarray[double, ndim=3, mode="c"] temp

    shape_mem.resize(n_arrays)  
    for i in range(n_arrays):
        print "i:", i
        temp = list_of_arrays[i]
        data.push_back(&temp[0,0,0])
        for j in range(3):
            shape_mem[i].push_back(temp.shape[j])
        shape.push_back(shape_mem[i].data())

    x = my_func_c(data.data(), shape.data(), n_arrays)

    return x

您的设置也需要修改:

# setup.py    
from distutils.core import setup, Extension
from Cython.Build import cythonize
import numpy as np

setup(ext_modules=cythonize(Extension(
            name='my_func_c',
            language='c++',
            extra_compile_args=['-std=c++11'],
            sources = ["my_func_c.pyx", "my_func.c"],
            include_dirs=[np.get_include()]
    )))

我更喜欢使用std::vector.data() 而不是&amp;data[0],因为第二个对于空的data 意味着未定义的行为,这就是我们需要std=c++11 标志的原因。

但最终,由您决定,做出哪种权衡:C++ 的额外复杂性(它有它自己的缺陷)与手工内存管理与暂时放弃类型安全.

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多