簡體   English   中英

使用 cython 將 numpy 數組列表傳遞給 C

[英]Passing list of numpy arrays to C using cython

我有一個 3D numpy 數組的列表list_of_arrays ,我想用模板傳遞給 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 數組而不進行 malloc 和釋放。

# 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

NB

要查看一個工作示例,我們可以使用一個非常簡單的函數來計算列表中所有數組的乘積。

# 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

一種替代方法是讓 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和 copy。 我還建議將free放在finally塊中以確保它們運行。


編輯: @ead 有幫助地指出numpy 形狀存儲為np.intp_t - 即一個足以容納指針的有符號整數,主要是 64 位 - 而int通常是 32 位。 因此,要在不復制的情況下傳遞形狀,您需要更改 C api。 轉換幫助使該錯誤更難以發現(“隱藏邏輯錯誤的好方法”)

我認為這是從 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()不是&data[0]因為第二個意味着空data未定義行為,這就是我們需要std=c++11標志的原因。

但最終,由您來決定,進行哪種權衡:C++ 的額外復雜性(它有自己的陷阱)與手工內存管理與暫時放棄類型安全。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM