Cython 与 C++ 接口:大型数组的分段错误
Cython interfaced with C++: segmentation fault for large arrays
我正在将我的代码从使用 ctypes 接口的 Python/C 转移到使用 Cython 接口的 Python/C++。新接口将使我更容易维护代码,因为我可以利用所有 C++ 功能并且需要相对较少的接口代码行。
接口代码与小型数组完美配合。然而,它在使用大型数组时会遇到 分段错误 。我一直在思考这个问题,但还没有接近解决方案。我已经包含了一个发生分段错误的最小示例。请注意,它一直出现在 Linux 和 Mac 上,而且 valgrind 也没有给出见解。另请注意,完全相同的示例在纯 C++ 中确实可以正常工作。
该示例包含(部分)C++ 中的稀疏矩阵 class。在 Cython 中创建了一个接口。因此 class 可以从 Python 使用。
C++ 端
sparse.h
#ifndef SPARSE_H
#define SPARSE_H
#include <iostream>
#include <cstdio>
using namespace std;
class Sparse {
public:
int* data;
int nnz;
Sparse();
~Sparse();
Sparse(int* data, int nnz);
void view(void);
};
#endif
sparse.cpp
#include "sparse.h"
Sparse::Sparse()
{
data = NULL;
nnz = 0 ;
}
Sparse::~Sparse() {}
Sparse::Sparse(int* Data, int NNZ)
{
nnz = NNZ ;
data = Data;
}
void Sparse::view(void)
{
int i;
for ( i=0 ; i<nnz ; i++ )
printf("(%3d) %d\n",i,data[i]);
}
Cython 界面
csparse.pyx
import numpy as np
cimport numpy as np
# UNCOMMENT TO FIX
#from cpython cimport Py_INCREF
cdef extern from "sparse.h":
cdef cppclass Sparse:
Sparse(int*, int) except +
int* data
int nnz
void view()
cdef class PySparse:
cdef Sparse *ptr
def __cinit__(self,**kwargs):
cdef np.ndarray[np.int32_t, ndim=1, mode="c"] data
data = kwargs['data'].astype(np.int32)
# UNCOMMENT TO FIX
#Py_INCREF(data)
self.ptr = new Sparse(
<int*> data.data if data is not None else NULL,
data.shape[0],
)
def __dealloc__(self):
del self.ptr
def view(self):
self.ptr.view()
setup.py
from distutils.core import setup, Extension
from Cython.Build import cythonize
setup(ext_modules = cythonize(Extension(
"csparse",
sources=["csparse.pyx", "sparse.cpp"],
language="c++",
)))
Python边
import numpy as np
import csparse
data = np.arange(100000,dtype='int32')
matrix = csparse.PySparse(
data = data
)
matrix.view() # --> segmentation fault
至运行:
$ python setup.py build_ext --inplace
$ python example.py
请注意 data = np.arange(100,dtype='int32')
确实有效。
内存由您的 numpy 数组管理。一旦它们超出范围(很可能在 PySparse
构造函数的末尾),数组就不复存在,所有指针都无效。这适用于大数组和小数组,但大概你只是幸运地使用了小数组。
您需要保存对您在 PySparse
对象的生命周期中使用的所有 numpy 数组的引用:
cdef class PySparse:
# ----------------------------------------------------------------------------
cdef Sparse *ptr
cdef object _held_reference # added
# ----------------------------------------------------------------------------
def __cinit__(self,**kwargs):
# ....
# your constructor code code goes here, unchanged...
# ....
self._held_reference = [data] # add any other numpy arrays you use to this list
通常,当您处理 C/C++ 指针时,您需要非常认真地思考谁拥有什么,这与正常的 Python 方法相比是一个很大的变化。从 numpy 数组获取指针 不会 复制数据并且它 不会 给 numpy 任何表明您仍在使用数据的迹象。
编辑说明: 在我的原始版本中,我尝试使用 locals()
作为收集我想要保留的所有数组的集合的快速方法。不幸的是,这似乎不包括 cdef
ed 数组,因此它无法保留您实际使用的数组(请注意,除非您另有说明,否则 astype()
会制作副本,所以您需要保留对副本的引用,而不是作为参数传入的原始文件)。
我正在将我的代码从使用 ctypes 接口的 Python/C 转移到使用 Cython 接口的 Python/C++。新接口将使我更容易维护代码,因为我可以利用所有 C++ 功能并且需要相对较少的接口代码行。
接口代码与小型数组完美配合。然而,它在使用大型数组时会遇到 分段错误 。我一直在思考这个问题,但还没有接近解决方案。我已经包含了一个发生分段错误的最小示例。请注意,它一直出现在 Linux 和 Mac 上,而且 valgrind 也没有给出见解。另请注意,完全相同的示例在纯 C++ 中确实可以正常工作。
该示例包含(部分)C++ 中的稀疏矩阵 class。在 Cython 中创建了一个接口。因此 class 可以从 Python 使用。
C++ 端
sparse.h
#ifndef SPARSE_H
#define SPARSE_H
#include <iostream>
#include <cstdio>
using namespace std;
class Sparse {
public:
int* data;
int nnz;
Sparse();
~Sparse();
Sparse(int* data, int nnz);
void view(void);
};
#endif
sparse.cpp
#include "sparse.h"
Sparse::Sparse()
{
data = NULL;
nnz = 0 ;
}
Sparse::~Sparse() {}
Sparse::Sparse(int* Data, int NNZ)
{
nnz = NNZ ;
data = Data;
}
void Sparse::view(void)
{
int i;
for ( i=0 ; i<nnz ; i++ )
printf("(%3d) %d\n",i,data[i]);
}
Cython 界面
csparse.pyx
import numpy as np
cimport numpy as np
# UNCOMMENT TO FIX
#from cpython cimport Py_INCREF
cdef extern from "sparse.h":
cdef cppclass Sparse:
Sparse(int*, int) except +
int* data
int nnz
void view()
cdef class PySparse:
cdef Sparse *ptr
def __cinit__(self,**kwargs):
cdef np.ndarray[np.int32_t, ndim=1, mode="c"] data
data = kwargs['data'].astype(np.int32)
# UNCOMMENT TO FIX
#Py_INCREF(data)
self.ptr = new Sparse(
<int*> data.data if data is not None else NULL,
data.shape[0],
)
def __dealloc__(self):
del self.ptr
def view(self):
self.ptr.view()
setup.py
from distutils.core import setup, Extension
from Cython.Build import cythonize
setup(ext_modules = cythonize(Extension(
"csparse",
sources=["csparse.pyx", "sparse.cpp"],
language="c++",
)))
Python边
import numpy as np
import csparse
data = np.arange(100000,dtype='int32')
matrix = csparse.PySparse(
data = data
)
matrix.view() # --> segmentation fault
至运行:
$ python setup.py build_ext --inplace
$ python example.py
请注意 data = np.arange(100,dtype='int32')
确实有效。
内存由您的 numpy 数组管理。一旦它们超出范围(很可能在 PySparse
构造函数的末尾),数组就不复存在,所有指针都无效。这适用于大数组和小数组,但大概你只是幸运地使用了小数组。
您需要保存对您在 PySparse
对象的生命周期中使用的所有 numpy 数组的引用:
cdef class PySparse:
# ----------------------------------------------------------------------------
cdef Sparse *ptr
cdef object _held_reference # added
# ----------------------------------------------------------------------------
def __cinit__(self,**kwargs):
# ....
# your constructor code code goes here, unchanged...
# ....
self._held_reference = [data] # add any other numpy arrays you use to this list
通常,当您处理 C/C++ 指针时,您需要非常认真地思考谁拥有什么,这与正常的 Python 方法相比是一个很大的变化。从 numpy 数组获取指针 不会 复制数据并且它 不会 给 numpy 任何表明您仍在使用数据的迹象。
编辑说明: 在我的原始版本中,我尝试使用 locals()
作为收集我想要保留的所有数组的集合的快速方法。不幸的是,这似乎不包括 cdef
ed 数组,因此它无法保留您实际使用的数组(请注意,除非您另有说明,否则 astype()
会制作副本,所以您需要保留对副本的引用,而不是作为参数传入的原始文件)。