注意: 虽然 JavaScript 对于本网站不是必需的,但您与内容的互动将受到限制。请开启 JavaScript 以获得完整的体验。

调试引用计数问题

警告

此页面因历史原因保留,可能包含过时或不正确的信息。

调试引用计数问题

发件人:Guido van Rossum <guido@CNRI.Reston.VA.US>
收件人:python-list@cwi.nl
日期:1998年5月27日 星期三 11:09:40 -0400

Mike Fletcher 发布了一些关于调试因引用计数问题而崩溃的 C 代码的帖子。他调试这个问题的方法似乎很典型,但我认为效率不高,所以我想提出一个不同的方法。基本上,仔细阅读你的代码并进行推理通常比使用一堆通用调试技术更有效。(这些技术非常有用,但只有当你已经充分隔离了问题之后才行。)

Mike 写道:

PyErr_Print() 让我知道我在节点的 GI 上遇到了 KeyError(它只作为任何字典中的_值_出现)。于是,我想(在 Guido 的提示下)这是一个引用计数错误……所以我继续前进,说“该死的内存泄漏”,到处添加 Py_INCREF。没用 :( 行为完全一样。

嗯……这听起来像是用自动武器来杀蚊子。在选择武器之前,先了解你的敌人。问题当然在于“到处”是什么意思。你可能很容易错过一个关键的地方,因为你没有想到它。

你应该首先重新阅读 Python/C API 手册的第 1.2.1 节,然后仔细阅读你正在调用的函数的描述。(我知道,手册不完整;但它*没有*那么不完整,如果你发现手册中没有的函数,阅读其源代码通常会提供线索。)

于是我说(开始自言自语),为什么不打印函数运行的环境,看看发生了什么……说干就干。然后错误消失了!删除打印行——错误再次出现(难以置信地重复三四次)。

这是海森堡定律应用于程序的典型例子:你无法在不影响事物的情况下观察事物。

我正在使用
printf(" Env as rule called:\n\t%s\n",
       PyString_AsString(PyObject_Repr(env)));

这会创建一个永远不会被回收的新字符串对象:PyObject_Repr() 返回的新字符串对象。由于这大概是一个大字符串,并且你正在分配很多这样的字符串(每次到达此打印语句时一个),你的应用程序的 malloc 模式会变得非常不同,这意味着你可能会看到非常不同的行为。

于是,(也许是震惊),我取消了 Py_INCREF,只用打印……仍然完美工作(除了我正在在 while 循环的每次迭代中打印整个解析树(这不好……))。

显然,你添加的 INCREF 并没有改变你程序的分配行为——所以它们显然不在正确的位置。你之前所说的证实了这一点:添加 INCREF 调用并没有解决问题。

所以,我目前的问题是

1) sys.refcount 的 C API 等价物是什么?(这样我就可以观察跨调用的引用计数,并确定哪些是引用中立的)

(Mark Hammond 也提到了这一点,他认为引用计数是对象的前两个字节——实际上是前四个字节,这表明他正在使用小端机器,否则他会说它是第三个和第四个字节。:-))

引用计数是 ob_refcnt 字段。但我不认为这会对你有很大帮助。如果一个对象的引用计数在调用期间没有改变,这并不意味着该调用是引用计数中立的——它可能会存储该对象的一个副本。

例如,以 PyList_SetItem(list, index, item) 为例。它不会改变列表或项目的引用计数,但它远非引用计数中立:它对列表是中立的,但它会从项目中窃取一个引用,并且它期望你将引用计数已增加的项目传递给它。(这个特定函数及其伙伴 PyTuple_SetItem() 最常用于用初始引用计数为 1 的新对象初始化列表/元组,这与它们的行为非常匹配。)

另一方面,PySequence_SetItem(list, index, item) *确实*会增加项目的引用计数。它被认为是引用计数中立的。(但它不适用于元组,元组是不可变的;这就是你需要 PyTuple_SetItem() 的原因。)

2) 打印到底发生了什么?我是否在需要对象之前调用 repr(),从而以某种方式拯救了它免于不光彩的销毁?这会不会是插入到字典中的对象的引用计数问题(鉴于 PyDict_SetItem 据说会存储它自己的对象引用,这似乎不太可能)。

正如我所说,这不是打印,而是 repr() 调用。我不期望 repr() 会保存对你的对象的引用,除非你自己实现了对象类型(那么这可能是你的 tp_repr 或 tp_str 函数中的一个错误)。

3) 还有谁对字节码到 C 转换器(如前段时间在列表中讨论的)变得*非常*感兴趣?:)

[不幸的是,由于 Python 的动态特性,这不会像你希望的那样有帮助。例如,对于表达式“a+b”,它必须生成对 PyNumber_Add(a, b) 的调用,因为它无法在没有*大量*(我指的是很多)类型推断工作的情况下知道 a 和 b 的类型。]

后来,Mike 写道:

好的,尝试调试这个奇怪的堆栈损坏问题时,我想到了一些思路:

1) 堆栈应该只在不应被 decref 的对象被 decref 时,或在没有引用计数的情况下创建对象时才会被破坏?

不——堆栈损坏也可能来自未初始化指针变量的使用,或越界索引。你的代码中可能存在一些相当微妙的差一错误!

2) 只有当你担心内存泄漏时才需要 decref 对象,既然我只是在调试,我现在不担心

你在这里对自己做了很大的坏事。当然,核心转储比内存泄漏更严重,但内存泄漏并不更容易找到——事实上,它们可能更难找到,因为它们隐藏在其他完全正常工作的代码中。在循环中被触发的内存泄漏可以使你的内存增长得如此之快,以至于你别无选择,只能立即开始调试!

正确的方法是尝试确保你在每个地方都有正确的 INCREF 和 DECREF 调用——唯一的方法是从手册中了解你调用的每个函数(包括你自己编写的函数!)的引用计数行为。

3) 如果我注释掉所有 DECREF 调用,我应该只需要担心我创建的没有引用计数的对象?那么,如果我在创建新对象的地方都添加一个 incref,我应该会有一个巨大的内存泄漏,但没有堆栈损坏。

不,不是这样。当一个对象被创建时,它已经有一个引用计数为一。API 手册对这种情况的描述是你“拥有”一个引用。(你并不拥有该对象——它可能被共享。例如,小整数和短字符串被积极地缓存和共享——但这不会影响你是否拥有它们的引用。)许多从其他对象中提取对象的例程也会让你负责拥有对该对象的引用,例如 PyObject_GetAttr() 和 PyObject_GetItem()。

另一方面(这些是最常见的例子,但不是唯一的例子),PyList_GetItem()、PyTuple_GetItem()、PyDict_GetItem() 和 PyDict_GetItemString() 都返回一个对象,但你没有拥有对该对象的引用。这被称为“借用”引用。当你将一个借用引用传递给另一个期望你 INCREF 其参数的调用(如上面讨论的 PyList_SetItem())时,你就会遇到问题。

我怀疑你的问题原因可能是这些情况之一,但由于你不发布你的代码,我无法提供更多帮助——我甚至不知道你正在调用哪些函数。也许你可以列出你正在调用的 Py* 函数,以及在手册中查找后你对它们的引用计数行为的任何问题?

当然,这没有奏效,否则我也不会因此打扰大家了。现在我正在将这个东西分解成更小的函数,看看这是否有助于追踪错误(尽管这几乎肯定会减慢函数的运行速度)。有没有关于引用计数问题的常见问题解答?

实际上没有什么能取代理解你正在使用的每个函数的引用计数行为。Python/C API 手册是你的朋友。(我保证,当你发现具体信息缺失或难以找到时,我会修复它。)