令人困惑的引用所有权:如何正确释放(通过Py_DECREF)对象的对象



我正在分析以下代码,该代码可以正确编译和运行,但会产生内存泄漏。

cfiboheap是斐波那契堆的 C 实现,以下代码是 cfiboheap 的 Cython 包装器(其中的一部分)。

我的疑问始于插入功能。对象data已在某处创建并传递给函数 insert() 。由于函数想要将此对象添加到 fiboheap 中,因此它会增加其引用计数。但之后呢?所有权归谁所有?在我的理解中,C 函数fh_insertkey()只是借用所有权。然后它返回一个需要封装的专有指针,然后由insert()返回。凉。但是我的对象data及其引用计数?通过创建胶囊,我正在创建一个新对象,但我并没有减少data的引用计数。这会产生内存泄漏。

(请注意,在返回insert()之前注释掉Py_INCREF或添加Py_DECREF会导致分段错误。

我的问题是:

1) 为什么在insert()期间需要增加data的引用计数?

2)为什么在extract()期间不需要使用Py_DECREF

3)更一般地说,在C和Python之间跳转时,如何准确地跟踪引用所有权?

4) 如何正确释放像这样的 FiboHeap 对象?我是否应该在__dealloc__()预防性使用Py_XDECREF,如果是,如何使用?

谢谢!

cimport cfiboheap
from cpython.pycapsule cimport PyCapsule_New, PyCapsule_GetPointer
from python_ref cimport Py_INCREF, Py_DECREF 
cdef inline object convert_fibheap_el_to_pycapsule(cfiboheap.fibheap_el* element):
    return PyCapsule_New(element, NULL, NULL)
cdef class FiboHeap:
    def __cinit__(FiboHeap self):
        self.treeptr = cfiboheap.fh_makekeyheap()
        if self.treeptr is NULL:
            raise MemoryError()
    def __dealloc__(FiboHeap self):
        if self.treeptr is not NULL:
            cfiboheap.fh_deleteheap(self.treeptr)
    cpdef object insert(FiboHeap self, double key, object data=None):
        Py_INCREF(data)
        cdef cfiboheap.fibheap_el* retValue = cfiboheap.fh_insertkey(self.treeptr, key, <void*>data)
        if retValue is NULL:
            raise MemoryError()
        return convert_fibheap_el_to_pycapsule(retValue)
    cpdef object extract(FiboHeap self):
        cdef void* ret = cfiboheap.fh_extractmin(self.treeptr)
        if ret is NULL:
            raise IndexError("FiboHeap is empty")
        return <object> ret
    cpdef object decrease_key(FiboHeap self,  object element, double newKey):
        cdef void* ret = cfiboheap.fh_replacekey(self.treeptr, convert_pycapsule_to_fibheap_el(element), newKey)
        if ret is NULL:
            raise IndexError("New Key is Bigger")
        return <object> ret 

请注意,这不是我写的,但我使用此示例来更好地理解 obj 引用并阻止泄漏(因为我实际上正在使用代码)。

使用 FiboHeap(以及发生泄漏的位置)的主代码如下所示:

cdef dijkstra(Graph G, int start_idx, int end_idx):
    cdef np.ndarray[object, ndim=1] fiboheap_nodes = np.empty([G.num_nodes], dtype=object) # holds all of our FiboHeap Nodes Pointers
    Q = FiboHeap()
    fiboheap_nodes[start_idx] = Q.insert(0, start_idx)
    # Then occasionally:
    Q.insert(...)
    Q.decrease_key(...)
    Q.extract()
    return

extract不是偷看,而是适当的流行,因此它正在删除 C fiboheap 中的 C 元素。

结论:似乎很明显,data的引用计数会导致内存泄漏,但为什么呢?以及如何阻止它?

1) 有必要以 insert 为单位增加引用计数,因为它的引用计数将在插入结束时自动减少。Cython 不知道您正在存储对象以供以后使用。(您可以检查生成的 C 代码以查看函数末尾的DECREF)。如果使用引用计数为 1 的对象调用 insert(即 .insert(SomeObject()),那么对象将在插入结束时被销毁,没有INCREF

2)如果在extract期间将物体从cfiboheap中移走,那么您应该做一个DECREF来确认您不再持有它的事实。首先将其强制转换为对象(因此您仍然持有对它的引用)

   cdef void* ret = cfiboheap.fh_extractmin(self.treeptr) # refcount is 1 here (from the INCREF when it was stored)
   if ret==NULL:
        # ...
   ret_obj = <object>ret
   # reference count should be 2 here - one for being on the heap and one for ret_obj. Casting to object increases the refcount in Cython
   Py_DECREF(ret_obj) # 1 here
   return ret_obj

3)老实说,如果你能避免的话,你尽量不要使用PyObject*!让Cython做这项工作要好得多。如果无法避免,则只需确保在存储对象时调用一次INCREF,并在停止存储对象时调用一次DECREF

4)您确实需要在__dealloc__中取消堆上的剩余对象。一个非常简单的方法可能是所有extract,直到cfiboheap为空:

try:
    while True:
        self.extract()
except IndexError:
    pass # ignore the error - we're done emptying the heap

关于使用胶囊的评论:谁拥有他们指向的fibheap_el(以及何时被破坏)?如果在cfiboheap被销毁时它被破坏,那么您会遇到带有无效指针的胶囊仍然处于活动状态的问题。在某处使用此胶囊最终可能会导致问题。如果它没有被cfiboheap破坏,那么你可能会有另一个内存泄漏。

相关内容

  • 没有找到相关文章

最新更新