除錯引用計數問題
警告
此頁面保留在此處出於歷史原因,可能包含過時或不正確的資訊。
除錯引用計數問題
發件人: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 手冊是您的朋友。(我保證在您發現特定資訊丟失或難以找到時修復它。)