Python 扩展在操作大型列表时会创建无效指针

Posted

技术标签:

【中文标题】Python 扩展在操作大型列表时会创建无效指针【英文标题】:Python extension creates invalid pointers when manipulating large lists 【发布时间】:2015-05-02 02:41:16 【问题描述】:

我设法为 python 列表实现了一个 Fisher-Yates shuffle 函数,作为习惯于扩展 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() 解决这个问题,但我不知道在哪里。我不认为我在创建任何对象,只是移动它们。那么内存问题是从哪里来的呢?

【问题讨论】:

这不是Fisher-Yates shuffle,你不应该在整个集合中选择随机元素,只能在光标(排除)和列表末尾之间选择。有了你所拥有的,你可能会用它自己交换一个元素,可能会做一些有趣的事情(但我根本不熟悉 python API,所以......) @Mat 啊,这是真的。我有点太快地阅读了伪代码。不过,我认为在这种情况下它不会产生任何功能差异。 令人惊讶的是,它确实有很大的不同。见blog.codinghorror.com/the-danger-of-naivete @Mat 哦,整洁。因此,由于函数给出的可能排序的数量既大于排列数量又不能被排列数量整除,因此某些排列被过度表示。 【参考方案1】:

在将它们传递给PyList_SetItem() 之前,您需要Py_XINCREF()。此外,捕捉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

有关详细信息,请参阅list API documentation。

作为进一步的建议,您可以考虑不直接针对 Python 扩展 API 进行编程。完成任何事情都需要大量代码,而且要保持引用计数正确是非常困难的。到目前为止,还有多种其他方法可以将 Python 与 C 或 C++ 接口。 CFFI 似乎是 Python 生态系统将标准化的低级接口。不过,SIP 和 SWIG 可能会为 C++ 提供更好的支持。有关 SIP 示例,请参阅this answer。

【讨论】:

【参考方案2】:

除了引用计数错误之外,您的扩展功能还有更多问题,更多如下:


虽然具有适当引用计数的 PyList_SetItem 是首选方法,但(丑陋的)选项是使用可以避免执行 INCREF 的 PyList_SET_ITEM 宏:

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 文档states the following:

当一个函数因为它调用的某个函数失败而必须失败时,它 一般不设置错误指示符;它调用的函数 已经设置好了。它负责处理错误和 清除异常或清除任何资源后返回 持有(例如对象引用或内存分配); 不应该 如果它不准备处理错误,则正常继续。如果 由于错误而返回,向调用者指示很重要 已设置错误。如果错误没有处理或仔细 传播后,对 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;

【讨论】:

是的。为了简洁起见,我忽略了错误检查。当我弄清楚如何传递参数时,当 PyList_Size 返回 -1 时它并没有失败,它只是跳过了 for 循环。如果 Py_ssize_t 未签名,程序可能会崩溃 失败,从PyList_Size返回-1表示抛出异常,你必须有序处理,要么清除异常或将其转发 - 您只是尚未满足代码中的适当条件:D 啊,哎呀,我错过了关于设置内部异常的部分。

以上是关于Python 扩展在操作大型列表时会创建无效指针的主要内容,如果未能解决你的问题,请参考以下文章

Julia:将 DataFrame 传递给函数会创建指向 DataFrame 的指针?

连接数据框会创建太多列

当我们创建没有扩展名的新文件时,会创建啥类型的文件?

在 C 中创建大型数组时出现分段错误

ORA-00904: 无效的标识符,当列标题正确时

扩展语法是不是会创建误报类型安全返回的对象?