Расширение Python создает недопустимые указатели при работе с большими списками

Мне удалось реализовать функцию перемешивания Фишера-Йейтса для списков Python в качестве упражнения для привыкания к расширению Python. Он отлично работает для относительно небольших списков, если только я не запускаю функцию несколько раз.

Всякий раз, когда размер списка превышает 100, я получаю всевозможные проблемы с памятью:

>>>import evosutil
>>> a=[i for i in range(100)]
>>> evosutil.shuffle(a)
>>> a
[52, 66, 0, 58, 41, 18, 50, 37, 81, 43, 74, 49, 90, 20, 63, 32, 89, 60, 2, 44, 3, 80, 15, 24, 22, 69, 86, 31, 56, 68, 34, 13, 38, 26, 14, 91, 73, 79, 39, 65, 5, 75, 84, 55, 7, 53, 93, 42, 40, 9, 51, 82, 29, 30, 99, 64, 33, 97, 27, 11, 6, 67, 16, 94, 95, 62, 57, 17, 78, 77, 71, 98, 72, 8, 88, 36, 85, 59, 21, 96, 23, 46, 10, 12, 48, 83, 4, 92, 45, 54, 1, 25, 19, 70, 35, 61, 47, 28, 87, 76]
>>> (Ctrl-D)
*** Error in `python3': free(): invalid next size (fast): 0x083fe680 ***

Или при попытке работать со списком из 1000 элементов:

*** Error in `python3': munmap_chunk(): invalid pointer: 0x083ff0e0 ***

Or,

Segmentation fault (core dumped)

Вот мой код модуля, выдающего ошибку:

inline void _List_SwapItems(PyObject* list, Py_ssize_t i1, Py_ssize_t i2){
    PyObject* tmp=PyList_GetItem(list, i2);
    PyList_SetItem(list, i2, PyList_GetItem(list, i1));
    PyList_SetItem(list, i1, tmp);
}

//Naive Fisher–Yates shuffle
static PyObject* shuffle(PyObject* self, PyObject* args){
    PyObject* list;
    PyArg_ParseTuple(args,"O", &list);
    unsigned seed = std::chrono::system_clock::now().time_since_epoch().count();
    std::minstd_rand0 rand(seed);
    Py_ssize_t size = PyList_Size(list);
    for(int i=0; i<size;++i){
        int randIndex = rand()%size;
        _List_SwapItems(list, randIndex, i);
    }
    Py_RETURN_NONE;
}

Я чувствую, что должен решить это либо с помощью free(), либо с помощью Py_DECREF() где-то, но я не вижу, где. Я не думаю, что создаю какие-либо объекты, просто перемещаю их. Так откуда берется проблема с памятью?


person SimLeek    schedule 02.05.2015    source источник
comment
Это не перетасовка Фишера-Йейтса, вы не должны выбирать случайный элемент во всем наборе, только между курсором (исключенным) и концом списка. С тем, что у вас есть, вы можете поменять местами элемент с самим собой, который может делать забавные вещи (но я вообще не знаком с API Python, так что...)   -  person Mat    schedule 02.05.2015
comment
@Mat Ах, это правда. Я слишком быстро прочитал псевдокод. Я не думаю, что это имеет какое-либо функциональное значение в этом случае.   -  person SimLeek    schedule 02.05.2015
comment
Удивительно, но разница очень большая. См. blog.codinghorror.com/the-danger-of-naivete.   -  person Mat    schedule 02.05.2015
comment
@Mat О, аккуратно. Таким образом, поскольку количество возможных порядков, заданных функцией, больше, чем количество перестановок, и не делится на них, некоторые перестановки представлены чрезмерно.   -  person SimLeek    schedule 02.05.2015


Ответы (2)


Вам нужно Py_XINCREF() оба объекта перед передачей их PyList_SetItem(). Кроме того, поймайте особый случай, когда i1 == i2:

inline void _List_SwapItems(PyObject* list, Py_ssize_t i1, Py_ssize_t i2){
    if (i1 == i2) {
        return;
    }
    PyObject* obj1=PyList_GetItem(list, i1);
    PyObject* obj2=PyList_GetItem(list, i2);
    Py_XINCREF(obj1);
    Py_XINCREF(obj2);
    PyList_SetItem(list, i2, obj1);
    PyList_SetItem(list, i1, obj2);
}

PyList_GetItem() возвращает заимствованную ссылку, т. е. он не INCREF возвращает возвращаемый объект. Если у вас нет других ссылок, счетчик ссылок будет равен 1 (поскольку на него ссылаются только из списка). Когда вы вызываете PyList_SetItem(list, i2, ...), список Py_XDECREF() представляет собой объект, ранее сохраненный в i2 (который вы сохраняете в tmp). В этот момент счетчик ссылок достигает 0 и объект освобождается. Упс.

Точно так же вы не можете просто вызвать PyList_SetItem(list, i, PyList_GetItem()), потому что SetItem крадет ссылку, которую вы ему передаете. Вы не владеете ссылкой, однако владеет «старый» список. Так что здесь вам тоже нужен Py_XINCREF.

Дополнительные сведения см. в документации по API списка.

В качестве еще одного предложения вы можете подумать о том, чтобы не программировать напрямую с API расширения Python. Требуется много кода, чтобы что-то сделать, и очень сложно поддерживать правильные счетчики ссылок. К настоящему времени существует множество других способов взаимодействия Python с C или C++. CFFI, по-видимому, является низкоуровневым интерфейсом, который стандартизирует экосистема Python. SIP и SWIG может предложить лучшую поддержку C++. Пример SIP см. в разделе этот ответ.

person Christian Aichinger    schedule 02.05.2015

В вашей функции расширения есть больше проблем, помимо ошибок подсчета ссылок, больше из них ниже:


В то время как PyList_SetItem с правильным подсчетом ссылок является предпочтительным способом, (уродливым) вариантом является использование PyList_SET_ITEM, который не требует выполнения INCREF:

void PyList_SET_ITEM(PyObject *list, Py_ssize_t i, PyObject *o)

Макроформа PyList_SetItem() без проверки ошибок. Обычно это используется только для заполнения новых списков, в которых нет предыдущего контента.

Примечание

Этот макрос «ворует» ссылку на элемент и, в отличие от PyList_SetItem(), не отбрасывает ссылку на какой-либо заменяемый элемент; любая ссылка в списке на позиции i будет удалена.

Таким образом, PyList_SET_ITEM не увеличивает и не уменьшает счетчики ссылок, что нам подходит, так как и изначально, и в конце элементы находятся в одном списке.

inline void _List_SwapItems(PyObject* list, Py_ssize_t i1, Py_ssize_t i2){
    PyObject* tmp = PyList_GET_ITEM(list, i2);
    PyList_SET_ITEM(list, i2, PyList_GET_ITEM(list, i1));
    PyList_SET_ITEM(list, i1, tmp);
}

Обратите внимание, что это вообще не проверяет ошибки, поэтому вам нужно убедиться, что ваш индекс находится в пределах границ (о чем заботится цикл for).


В вашем коде есть еще одна серьезная проблема, которая еще не обсуждается - полное отсутствие проверки ошибок. Например, при передаче в объекте, не входящем в список, вы должны поднять TypeError. Теперь код завершится ошибкой на PyList_Size, возвращая -1 и установка внутреннего исключения, это может привести к ошибочному поведению всех будущих расширений C:

Аналогичным образом PyArg_ParseTuple может и отказаться, если неправильный количество аргументов передается в, поэтому вы должны проверить его возвращаемое значение; в этом случае list может быть неинициализирован, и ваш код будет иметь совершенно неопределенное поведение.

В документации C-API указано следующее:

Когда функция должна завершиться ошибкой из-за того, что какая-то функция, которую она вызвала, не удалась, она обычно не устанавливает индикатор ошибки; функция, которую он вызвал, уже установила его. Он отвечает либо за обработку ошибки и очистку исключения, либо за возврат после очистки любых ресурсов, которые он содержит (таких как ссылки на объекты или выделение памяти); он не должен нормально продолжаться, если он не готов обработать ошибку. При возврате из-за ошибки важно указать вызывающей стороне, что была установлена ​​ошибка. Если ошибка не обработана или тщательно не распространена, дополнительные вызовы API Python/C могут вести себя не так, как предполагалось, и могут завершиться загадочным образом.

Таким образом, вот правильный способ написать вашу функцию расширения:

static PyObject* shuffle(PyObject* self, PyObject* args){
    PyObject* list;
    if (! PyArg_ParseTuple(args, "O", &list)) {
        // PyArg_ParseTuple set the proper exception
        return NULL;
    }

    if (! PyList_Check(list)) {
        PyErr_SetString(PyExc_TypeError,
            "bad argument to shuffle; list expected");
        return NULL;
    }

    unsigned seed = std::chrono::system_clock::now().time_since_epoch().count();
    std::minstd_rand0 rand(seed);

    Py_ssize_t size = PyList_Size(list);
    for(int i=0; i<size;++i){
        int randIndex = rand()%size;
        _List_SwapItems(list, randIndex, i);
    }
    Py_RETURN_NONE;
}
person Antti Haapala    schedule 02.05.2015
comment
Ага. Я проигнорировал проверку ошибок для краткости. Это не потерпело неудачу, когда PyList_Size вернул -1, когда я выяснял, как передать аргументы, хотя он просто пропустил цикл for. Если бы Py_ssize_t не был подписан, программа, вероятно, вылетела бы - person SimLeek; 03.05.2015
comment
Это не будет неудачным, -1, возвращенное из PyList_Size, означает, что было создано исключение, и вы должны обработать его упорядоченно, либо очистить исключение, либо передать его дальше — вы просто не выполнить надлежащее условие в вашем коде еще: D - person Antti Haapala; 03.05.2015
comment
Ах, упс, я пропустил часть об установке внутреннего исключения. - person SimLeek; 03.05.2015