Cython与C++接口:大型阵列的分段错误

Cython interfaced with C++: segmentation fault for large arrays

本文关键字:阵列 分段 错误 大型 C++ 接口 Cython      更新时间:2023-10-16

我正在将我的代码从使用 ctypes 接口的 Python/C 转移到使用 Cython 接口的 Python/C++。新界面将使我更容易维护代码,因为我可以利用所有C++功能,并且需要相对较少的界面代码行。

接口代码非常适合小数组。但是,在使用大型数组时会遇到分段错误。我一直在思考这个问题,但还没有更接近解决方案。我包括了一个发生分段错误的最小示例。请注意,它始终发生在Linux和Mac上,而且valgrind也没有提供见解。另请注意,纯C++中完全相同的示例确实可以正常工作。

该示例包含 C++ 中的稀疏矩阵类(部分)。在 Cython 中创建了一个接口。因此,可以从 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) %dn",i,data[i]);
}

赛通接口

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++",
)))

蟒蛇端

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() 作为收集我想要保留的所有数组集合的快速方法。不幸的是,这似乎不包括 ed 数组cdef因此它无法保留您实际使用的数组(请注意,除非您另有说明,否则astype()会制作副本,因此您需要保留对副本的引用,而不是作为参数传入的原始内容)。