Пустой указатель, указывающий на тот же адрес - PullRequest
0 голосов
/ 09 декабря 2018

Проблема

Пустой указатель на класс cdef указывает на тот же адрес памяти без принудительного счетчика ссылок python.

Описание

У меня есть простой класс, которыйЯ хочу сохранить в векторе cpp, приведя его к пустому указателю.Однако после печати адресов памяти, на которые указывает указатель, он повторяется после второй итерации , если только Я не увеличу счетчик ссылок, добавив новый объект в список.Может кто-нибудь, почему память возвращается к циклу без применения счетчика ссылок?

# 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 ]

0 голосов
/ 10 декабря 2018

Этот ответ стал довольно длинным, поэтому есть краткий обзор содержания:

  1. Объяснение наблюдаемого поведения
  2. Наивный подход во избежание проблемы
  3. Более систематическое и c ++ - типичное решение
  4. Иллюстрирование проблемы для многопоточного кода в режиме "nogil"
  5. Расширение c ++ - типичное решение для режима nogil

Объяснение наблюдаемого поведения

Сделка с Cython: если ваши переменные имеют тип object или наследуют от него (в вашем случае cdef Temp), Cython управляетподсчет ссылок для вас.Как только вы приведете его к PyObject * или любому другому указателю - ответственность за подсчет ссылок лежит на вас.

Очевидно, что единственная ссылка на созданный объект - это переменная tmp, как только вы перепривязаете егоДля вновь созданного Temp -объекта счетчик ссылок старого объекта становится 0, и он уничтожается - указатели в векторе становятся висящими.Тем не менее, одну и ту же память можно использовать повторно (это вполне вероятно), и поэтому вы всегда видите один и тот же повторно используемый адрес.

Наивное решение

Как вы могли бы сделать ссылкуподсчет?Например (я использую скорее 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)

Теперь все объекты остаются живыми и «умирают» только после явного вызова Py_XDECREF.

C ++ - типичное решение

Выше не очень типичный c ++ - способ работы, я бы предпочел ввести оболочку, которая управляет подсчетом ссылок автоматически (в отличие от 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

Примечательные вещи:

  1. PyObjectHolder увеличивает ref-counter, как только он овладевает PyObject -поинтером, и уменьшает его, как только освобождает указатель.
  2. Правило трех означает, что мы также должны позаботиться о конструкторе копирования и операторе присваивания.
  3. Я опустил move-stuff для c ++ 11, но вы должны позаботиться об этом.также.

Проблемы с режимом nogil

Однако есть одна очень важная вещь: Вы не должны выпускать GIL с вышеуказанной реализацией(то есть импортируйте его как PyObjectHolder(PyObject *o) nogil, но есть также проблемы, когда C ++ копирует векторы и тому подобное) - потому что в противном случае Py_XINCREF и Py_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 ссылок (+2 внутри sys.getrefcount) ссылок, поэтому этот объект никогда не будет уничтожен.

Создание PyObjectHolder поточно-ориентированного

Так что же делать?Самое простое решение - использовать мьютекс для защиты доступа к реф-счетчику (т.е. каждый раз, когда вызывается Py_XINCREF или Py_XDECREF).Недостатком этого подхода является то, что он может значительно замедлить одноядерный код (см., Например, эту старую статью о более старой попытке заменить 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

Однако, как указал @DavidW, используя std::mutexработает только для openmp-threads, но не для потоков, созданных Python-интерпретатором.

Вот пример, для которого мьютекс-решение потерпит неудачу.

Во-первых, оборачивая nogil-функцию в def -функцию:

%%cython --cplus -c=/openmp 
...
def single_create_vectors(object o):
    cdef PyObject *ptr=<PyObject *>o
    with nogil:
         create_vectors(ptr)

А теперь используя threading -модуль для создания

import sys
a=[1000]*10000  # some safety, so chances are high python will not crash 
print(sys.getrefcount(a[0]))  
#output: 10002  

from threading import Thread
threads = []
for i in range(100):
    t = Thread(target=single_create_vectors, args=(a[0],))
    threads.append(t)
    t.start()
for t in threads:
    t.join()

print(sys.getrefcount(a[0]))
#output: 10015   but should be 10002!

Альтернативаиспользование std::mutex будет означать использование механизма Python, то есть PyGILState_STATE, что приведет к коду, подобному

...
PyObjectHolderPy(PyObject *o):ptr(o){
    PyGILState_STATE gstate;
    gstate = PyGILState_Ensure();
    Py_XINCREF(ptr);
    PyGILState_Release(gstate);
}
...

Это также будет работать для threading -примера выше.Однако у PyGILState_Ensure слишком много служебной информации - для приведенного выше примера он будет примерно в 100 раз медленнее, чем решение мьютекса.Еще одно облегченное решение с Python-механизмом будет означать также гораздо больше хлопот.


Вывод полной версии небезопасной версии:

%%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)
0 голосов
/ 10 декабря 2018

Тот факт, что ваши объекты попадают по одному адресу, является совпадением.Ваша проблема в том, что ваши объекты Python уничтожаются, когда последняя ссылка на них Python исчезает.Если вы хотите сохранить живые объекты Python, вам нужно где-то хранить ссылку на них.

В вашем случае, поскольку tmp - это единственная ссылка на объект Temp, который вы создаете в цикле,каждый раз, когда вы переопределяете tmp, объект, на который он ссылался ранее, уничтожается.Таким образом, в памяти остается пустое пространство, которое точно соответствует нужному размеру, чтобы содержать объект Temp, который создается в следующей итерации цикла, что приводит к чередующемуся шаблону, который вы видите в своих указателях.

...