Расширение 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 ***

Или же,

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

3

Решение

Вам нужно 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. ГЛОТОК а также SWIG может предложить лучшую поддержку C ++, хотя. Для примера SIP см. этот ответ.

3

Другие решения

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


В то время как 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 утверждает следующее:

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

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

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;
}
3

По вопросам рекламы [email protected]