注意: 虽然 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(它只在任何字典中显示为 _value_)。因此,我想(在 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 也赞同,他认为引用计数是对象的前 2 个字节 - 实际上,它是前 4 个字节,这表明他是在小端机器上工作,否则他会说它是第 3 个和第 4 个字节。 :-))

引用计数是 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 不应该被执行,或者对象在开始时没有引用,则堆栈应该只会损坏?

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

2) 您只需要 decref 对象,如果您担心内存泄漏,因为我只是在调试,我目前不担心

您在这里给自己帮倒忙了。当然,核心转储比内存泄漏更严重,但内存泄漏并不容易找到 - 事实上,它们可能更难找到,因为它们隐藏在其他正常工作的代码中。恰好在循环中触发的内存泄漏会使您的内存增长如此之快,以至于您别无选择,只能从那里开始调试!

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

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

不,这不是它的工作原理。当创建对象时,它已经带有引用计数 1。API 手册中关于这种情况说,您“拥有”一个引用。(您不拥有该对象 - 它可能是共享的。例如,小整数和短字符串会被积极地缓存和共享 -- 但这不会影响您是否拥有对它们的引用。)许多从其他对象中提取对象的例程也让您有责任拥有对该对象的引用,例如 PyObject_GetAttr() 和 PyObject_GetItem()。

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

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

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

真的没有什么可以替代理解您正在使用的每个函数的引用计数行为。Python/C API 手册是您的朋友。(我保证在您发现特定信息丢失或难以找到时修复它。)