首页 文章

无效指针指向同一地址

提问于
浏览
1

问题

指向cdef类的void指针指向相同的内存地址,而不强制python引用计数器 .

说明

我有一个简单的类,我想通过将它转换为void指针存储在cpp向量中 . 但是,在打印指针指向的内存地址后,它会在第二次迭代后重复, unless 我通过将新对象添加到列表来强制增加引用计数器 . 有人为什么内存循环没有参考计数器执行?

# distutils: language = c++
# distutils: extra_compile_args = -std=c++11
from libcpp.vector cimport vector
from libc.stdio cimport printf

cdef class Temp:
    cdef int a
    def __init__(self, a):
        self.a = a


def f():
    cdef vector[void *] vec
    cdef int i, n = 3
    cdef Temp tmp
    cdef list ids = []
    # cdef list classes  = [] # force reference counter?
    for i in range(n):
        tmp = Temp(1)
        # classes.append(tmp)
        vec.push_back(<void *> tmp)
        printf('%p ', <void *> tmp)
        ids.append(id(tmp))
    print(ids)
f()

哪个输出:

[140137023037824, 140137023037848, 140137023037824]

但是,如果我通过将引用计数器添加到类列表来强制它:

[140663518040448, 140663518040472, 140663518040496]

2 回答

  • 2

    这个答案变得很长,因此可以快速浏览内容:

    • 观察到的行为的说明

    • 天真的方法来避免这个问题

    • 更系统,更典型的解决方案

    • 解释了"nogil" -mode中多线程代码的问题

    • 为nogil模式扩展c-ticalpical解决方案

    Explanation of the observed behavior

    与Cython的交易:只要您的变量属于 object 类型或从它继承(在您的情况下 cdef Temp ),cython将为您管理引用计数 . 只要将其转换为 PyObject * 或任何其他指针 - 引用计数就是您的责任 .

    显然,对创建的对象的唯一引用是变量 tmp ,只要将其重新绑定到新创建的 Temp object,旧对象的引用计数器就会变为 0 并且它被销毁 - 向量中的指针变得悬空 . 但是,可以重用相同的内存(很可能),因此您总是看到相同的重用地址 .

    Naive solution

    你怎么能做引用计数?例如(我使用 PyObject * 而不是 void * ):

    ...
    from cpython cimport PyObject,Py_XINCREF, Py_XDECREF    
    ...
    def f():
        cdef vector[PyObject *] vec
        cdef int i, n = 3
        cdef Temp tmp
        cdef PyObject *tmp_ptr
        cdef list ids = []
        for i in range(n):
            tmp = Temp(1)
            tmp_ptr = <PyObject *> tmp
            Py_XINCREF(tmp_ptr)   # ensure it is not destroyed
            vec.push_back(tmp_ptr)
            printf('%p ', tmp_ptr)
            ids.append(id(tmp))
    
        #free memory:
        for i in range(n):
            Py_XDECREF(vec.at(i))
        print(ids)
    

    现在所有对象都保持活动状态"die"只有在显式调用 Py_XDECREF 之后 .

    C++-typical solution

    以上不是一个非常典型的做法,我宁愿介绍一个自动管理引用计数的包装器(与 std::shared_ptr 不同):

    ...
    cdef extern from *:
        """
        #include <Python.h>
        class PyObjectHolder{
        public:
            PyObject *ptr;
            PyObjectHolder():ptr(nullptr){}
            PyObjectHolder(PyObject *o):ptr(o){
               Py_XINCREF(ptr);
            }
            //rule of 3
            ~PyObjectHolder(){
                Py_XDECREF(ptr);
            }
            PyObjectHolder(const PyObjectHolder &h):
                PyObjectHolder(h.ptr){}
            PyObjectHolder& operator=(const PyObjectHolder &other){
                Py_XDECREF(ptr);
                ptr=other.ptr;
                Py_XINCREF(ptr);
                return *this;
            }
        };
        """
        cdef cppclass PyObjectHolder:
            PyObjectHolder(PyObject *o)
    
    ...
    def f():
        cdef vector[PyObjectHolder] vec
        cdef int i, n = 3
        cdef Temp tmp
        cdef PyObject *tmp_ptr
        cdef list ids = []
        for i in range(n):
            tmp = Temp(1)
            vec.push_back(PyObjectHolder(<PyObject *> tmp)) # vector::emplace_back is missing in Cython-wrappers
            printf('%p ', <PyObject *> tmp)
            ids.append(id(tmp))
       print(ids) 
       # PyObjectHolder automatically decreases ref-counter as soon 
       # vec is out of scope, no need to take additional care
    

    值得注意的事情:

    • PyObjectHolder 只要拥有一个 PyObject -pointer就会增加ref-counter,并在释放指针后立即减少它 .

    • 三条法则意味着我们还必须注意复制构造函数和赋值运算符

    • 我省略了针对c 11的移动内容,但你也需要处理它 .

    Problems with nogil-mode

    然而,有一个非常重要的事情: You shouldn't release GIL 具有上述实现(即将其导入 PyObjectHolder(PyObject *o) nogil 但是当C复制向量时也存在问题) - 因为否则 Py_XINCREFPy_XDECREF 可能无法正常工作 .

    为了说明这一点,我们来看看下面的代码,它释放gil并且并行执行一些愚蠢的计算(整个魔术单元在答案的最后是列表中):

    %%cython --cplus -c=/openmp 
    ...
    # importing as nogil - A BAD THING
    cdef cppclass PyObjectHolder:
        PyObjectHolder(PyObject *o) nogil
    
    # some functionality using a lot of incref/decref  
    cdef int create_vectors(PyObject *o) nogil:
        cdef vector[PyObjectHolder] vec
        cdef int i
        for i in range(100):
            vec.push_back(PyObjectHolder(o))
        return vec.size()
    
    # using PyObjectHolder without gil - A BAD THING
    def run(object o):
        cdef PyObject *ptr=<PyObject*>o;
        cdef int i
        for i in prange(10, nogil=True):
            create_vectors(ptr)
    

    现在:

    import sys
    a=[1000]*1000
    print("Starts with", sys.getrefcount(a[0]))
    # prints: Starts with 1002
    run(a[0])
    print("Ends with", sys.getrefcount(a[0]))
    #prints: Ends with 1177
    

    我们很幸运,程序没有崩溃(但可能!) . 但是由于竞争条件,我们最终导致内存泄漏 - a[0] 的引用计数为 1177 但是只有1000个引用( sys.getrefcount 内部有2个)引用,因此该对象永远不会被销毁 .

    Making PyObjectHolder thread-safe

    那么该怎么办?最简单的解决方案是使用互斥锁来保护对ref-counter的访问(即每次调用 Py_XINCREFPy_XDECREF 时) . 这种方法的缺点是它可能会显着降低单核代码的速度(例如,参见this old article关于通过类似互斥的方法替换GIL的旧尝试) .

    这是一个原型:

    %%cython --cplus -c=/openmp 
    ...
    cdef extern from *:
        """
        #include <Python.h>
        #include <mutex>
    
        std::mutex ref_mutex;
    
        class PyObjectHolder{
        public:
            PyObject *ptr;
            PyObjectHolder():ptr(nullptr){}
            PyObjectHolder(PyObject *o):ptr(o){
                std::lock_guard<std::mutex> guard(ref_mutex);
                Py_XINCREF(ptr);
            }
            //rule of 3
            ~PyObjectHolder(){
                std::lock_guard<std::mutex> guard(ref_mutex);
                Py_XDECREF(ptr);
            }
            PyObjectHolder(const PyObjectHolder &h):
                PyObjectHolder(h.ptr){}
            PyObjectHolder& operator=(const PyObjectHolder &other){
                {
                    std::lock_guard<std::mutex> guard(ref_mutex);
                    Py_XDECREF(ptr);
                    ptr=other.ptr;
                    Py_XINCREF(ptr);
                }
                return *this;
            }
        };
        """
        cdef cppclass PyObjectHolder:
            PyObjectHolder(PyObject *o) nogil
        ...
    

    现在,运行从上面剪切的代码会产生预期/正确的行为:

    import sys
    a=[1000]*1000
    print("Starts with", sys.getrefcount(a[0]))
    # prints: Starts with 1002
    run(a[0])
    print("Ends with", sys.getrefcount(a[0]))
    #prints: Ends with 1002
    

    列出完整的线程不安全版本:

    %%cython --cplus -c=/openmp 
    
    from libcpp.vector cimport vector
    from libc.stdio cimport printf
    from cpython cimport PyObject  
    from cython.parallel import prange
    
    import sys
    
    cdef extern from *:
        """
        #include <Python.h>
    
        class PyObjectHolder{
        public:
            PyObject *ptr;
            PyObjectHolder():ptr(nullptr){}
            PyObjectHolder(PyObject *o):ptr(o){
                Py_XINCREF(ptr);
            }
            //rule of 3
            ~PyObjectHolder(){
                Py_XDECREF(ptr);
            }
            PyObjectHolder(const PyObjectHolder &h):
                PyObjectHolder(h.ptr){}
            PyObjectHolder& operator=(const PyObjectHolder &other){
                {
                    Py_XDECREF(ptr);
                    ptr=other.ptr;
                    Py_XINCREF(ptr);
                }
                return *this;
            }
        };
        """
        cdef cppclass PyObjectHolder:
            PyObjectHolder(PyObject *o) nogil
    
    
    cdef int create_vectors(PyObject *o) nogil:
        cdef vector[PyObjectHolder] vec
        cdef int i
        for i in range(100):
            vec.push_back(PyObjectHolder(o))
        return vec.size()
    
    def run(object o):
        cdef PyObject *ptr=<PyObject*>o;
        cdef int i
        for i in prange(10, nogil=True):
            create_vectors(ptr)
    
  • 3

    您的对象最终位于同一地址的事实是巧合 . 你的问题是你的python对象在它们的最后一个python引用消失时被销毁 . 如果你想让python对象保持活着,你需要在某处保存对它们的引用 .

    在您的情况下,由于 tmp 是您在循环中创建的 Temp 对象的唯一引用,因此每次重新分配 tmp 时,它先前引用的对象都将被销毁 . 这会在内存中留下一个空白空间,它可以很方便地保存在循环的下一次迭代中创建的 Temp 对象的正确大小,从而导致您在指针中看到的交替模式 .

相关问题