Debugging Referenzzählprobleme
Warnung
Diese Seite bleibt aus historischen Gründen erhalten und kann veraltete oder falsche Informationen enthalten.
Debugging Referenzzählprobleme
Von: Guido van Rossum <guido@CNRI.Reston.VA.US>
An: python-list@cwi.nl
Datum: Mi, 27. Mai 1998 11:09:40 -0400
Mike Fletcher schrieb eine Reihe von Beiträgen über das Debugging von C-Code, der abstürzt, wahrscheinlich wegen Problemen mit der Referenzzählung. Sein Ansatz zur Fehlersuche bei diesem Problem scheint typisch, aber ich denke, er ist nicht sehr produktiv, daher möchte ich einen anderen Ansatz vorschlagen. Grundsätzlich ist es oft produktiver, seinen Code sorgfältig zu lesen und darüber nachzudenken, als eine Reihe allgemeiner Debugging-Techniken anzuwenden. (Diese Techniken sind sehr nützlich, aber nur, wenn Sie das Problem ausreichend isoliert haben.)
Mike schreibt
PyErr_Print() ließ mich wissen, dass ich einen KeyError für die GI des Knotens bekomme (der nur als _value_ in irgendeinem Wörterbuch erscheint). Also dachte ich (mit Anstößen von Guido), es ist ein Refcount-Fehler... also forge ich vor und sage "egal die Speicherlecks", füge überall Py_INCREF's hinzu. Kein Erfolg :( Genau das gleiche Verhalten.
Hmm... Das klingt, als würde man eine automatische Waffe benutzen, um eine Mücke zu töten. Kenne deinen Feind, bevor du deine Waffe wählst. Das Problem liegt natürlich darin, wo "überall" ist. Sie können den einen entscheidenden Ort leicht übersehen, weil Sie nicht daran gedacht haben.
Sie sollten damit beginnen, Abschnitt 1.2.1 des Python/C API-Handbuchs noch einmal zu lesen und dann sorgfältig die Beschreibungen der Funktionen zu lesen, die Sie aufrufen. (Ich weiß, das Handbuch ist nicht vollständig; aber es ist nicht *so* unvollständig, und wenn Sie eine Funktion finden, die nicht im Handbuch steht, gibt das Lesen ihres Quellcodes normalerweise einen Hinweis.)
So sage ich (beginne, mit mir selbst zu sprechen): Warum nicht die Umgebung ausgeben, in der die Funktionen ausgeführt werden, um zu sehen, was vor sich geht... kaum gesagt, getan. Und der Fehler verschwand! Nehmen Sie die Druckzeile heraus – der Fehler taucht wieder auf (iterieren Sie drei- oder viermal ungläubig).
Dies ist ein typisches Beispiel für Heisenbergs Gesetz, angewendet auf Programme: Man kann etwas nicht beobachten, ohne es zu beeinflussen.
Ich benutzeprintf(" Env as rule called:\n\t%s\n",
PyString_AsString(PyObject_Repr(env)));
Dies erstellt ein neues String-Objekt, das nie gesammelt wird: das neue String-Objekt, das von PyObject_Repr() zurückgegeben wird. Da dies vermutlich ein großer String ist und Sie viele davon allozieren (einen jedes Mal, wenn Sie diese Druckanweisung erreichen), wird das Malloc-Muster Ihrer Anwendung sehr unterschiedlich, und dies bedeutet, dass Sie ein sehr unterschiedliches Verhalten sehen können.
Also, (vielleicht aus Schock), eliminiere ich die Py_INCREFs und versuche es nur mit dem Drucken... es funktioniert immer noch perfekt (abgesehen davon, dass ich den gesamten Parse-Baum bei jeder Iteration der while-Schleife drucke (was nicht gut ist...)).
Offenbar ändern die von Ihnen hinzugefügten INCREFs nicht das Allokationsverhalten Ihres Programms – also sind sie offensichtlich nicht an den richtigen Stellen. Dies wird durch das bestätigt, was Sie zuvor gesagt haben: Das Hinzufügen der INCREF-Aufrufe hat das Problem nicht behoben.
Meine Fragen des Tages1) Was ist das C-API-Äquivalent für sys.refcount? (damit ich Referenzzählungen über Aufrufe hinweg verfolgen und bestimmen kann, welche referenzneutral sind)
(Wiederholt von Mark Hammond, der glaubt, dass die Referenzzählung die ersten 2 Bytes des Objekts ist – tatsächlich sind es die ersten 4 Bytes, und das verrät, dass er auf einer Little-Endian-Maschine arbeitet, sonst hätte er gesagt, es seien die 3. und 4. Bytes. :-)
Die Referenzzählung ist das ob_refcnt-Feld. Aber ich glaube nicht, dass Ihnen das viel helfen wird. Wenn sich die Referenzzählung eines Objekts während eines Aufrufs nicht ändert, bedeutet das nicht, dass der Aufruf referenzzählungsneutral ist – er könnte eine Kopie des Objekts speichern.
Nehmen Sie zum Beispiel PyList_SetItem(list, index, item). Es ändert weder die Referenzzählung der Liste noch des Elements, aber es ist weit davon entfernt, referenzzählungsneutral zu sein: Es ist neutral für die Liste, aber es stiehlt eine Referenz vom Element und erwartet von Ihnen, dass Sie das Element mit bereits erhöhter Referenzzählung übergeben. (Diese spezielle Funktion und ihr Gegenstück, PyTuple_SetItem(), werden am häufigsten verwendet, um Listen/Tupel mit neuen Objekten zu initialisieren, die mit einer anfänglichen Referenzzählung von 1 erstellt wurden, was gut zu ihrem Verhalten passt.)
Andererseits inkrementiert PySequence_SetItem(list, index, item) *die* Referenzzählung des Elements. Und es gilt als referenzzählungsneutral. (Aber es funktioniert nicht für Tupel, die unveränderlich sind; deshalb brauchen Sie PyTuple_SetItem().)
2) Was zum Teufel passiert mit dem Drucken? Rette ich irgendwie das Objekt vor der unwürdigen Zerstörung, indem ich repr darauf aufrufe, kurz bevor ich es brauche? Könnte das ein Problem mit der Referenzzählung von Objekten sein, die in das Wörterbuch eingefügt werden (scheint unwahrscheinlich, da PyDict_SetItem angeblich eigene Referenzen auf Objekte speichert).
Wie gesagt, es ist nicht das Drucken, es ist der repr()-Aufruf. Ich erwarte nicht, dass repr() eine Referenz auf Ihr Objekt speichert, es sei denn, Sie haben den Objekttyp selbst implementiert (dann könnte es ein Fehler in Ihrer tp_repr- oder tp_str-Funktion sein).
3) Ist jemand anderes *wirklich* an einem Bytecode-zu-C-Übersetzer interessiert (wie vor einiger Zeit in der Liste diskutiert)? :)
[Leider wird das Ihnen nicht so viel helfen, wie Sie möchten, aufgrund der dynamischen Natur von Python. Zum Beispiel generiert es für den Ausdruck "a+b" einen Aufruf von PyNumber_Add(a, b), weil es die Typen von a und b ohne *viel* (und ich meine VIEL) Typinferenzaufwand nicht kennen kann.]
Später schreibt Mike
Okay, im Versuch, diese seltsame Stack-Korruptions-Sache zu debuggen, dachte ich in die Richtung von1) Stacks sollten nur korrumpiert werden, wenn ein Objekt decref'd wird, das es nicht sein sollte, oder ein Objekt erstellt wird, ohne dass eine Referenz darauf vorhanden ist?
Nein – korrupte Stacks können auch aus der Verwendung von nicht initialisierten Zeigervariablen oder Out-of-Bounds-Indizes resultieren. Es könnte ziemlich subtile Off-by-One-Fehler in Ihrem Code geben!
2) Man muss Objekte nur dann decref'n, wenn man sich Sorgen um Speicherlecks macht, da ich nur debugge, mache ich mir im Moment keine Sorgen.
Sie tun sich hier einen großen Nachteil an. Sicher, Core-Dumps sind ernstere Probleme als Speicherlecks, aber Speicherlecks sind nicht leichter zu finden – tatsächlich sind sie wahrscheinlich schwerer zu finden, weil sie sich in ansonsten perfekt funktionierendem Code verstecken. Ein Speicherleck, das zufällig in einer Schleife ausgelöst wird, kann Ihren Speicher so schnell vergrößern, dass Sie keine Wahl haben, als dort mit dem Debugging zu beginnen!
Der richtige Ansatz ist, sicherzustellen, dass Sie die richtigen INCREF- und DECREF-Aufrufe an jeder Stelle haben – und der einzige Weg ist, (aus dem Handbuch) das Referenzzählungsverhalten jeder Funktion zu kennen, die Sie aufrufen (einschließlich Funktionen, die Sie selbst geschrieben haben!).
3) Wenn ich alle DECREF-Aufrufe auskommentiere, sollte ich mich nur um Objekte kümmern, die ich erstellt habe und die keine Referenzzählungen haben? Wenn ich also überall, wo ein neues Objekt erstellt wird, ein incref hinzufüge, sollte ich ein riesiges Speicherleck haben, aber keine Stack-Korruption.
Nein, so funktioniert das nicht. Wenn ein Objekt erstellt wird, hat es bereits eine Referenzzählung von eins. Das API-Handbuch sagt in dieser Situation, dass Sie eine Referenz "besitzen". (Sie besitzen das Objekt nicht – es kann geteilt werden. Zum Beispiel werden kleine ganze Zahlen und kurze Zeichenketten aggressiv gecached und geteilt – aber das beeinflusst nicht, ob Sie eine Referenz darauf besitzen.) Viele Routinen, die Objekte aus anderen Objekten extrahieren, übertragen Ihnen auch die Verantwortung, eine Referenz auf das Objekt zu besitzen, z. B. PyObject_GetAttr() und PyObject_GetItem().
Andererseits (und das sind die häufigsten Beispiele, aber nicht die einzigen), PyList_GetItem(), PyTuple_GetItem(), PyDict_GetItem() und PyDict_GetItemString() geben Ihnen alle ein Objekt zurück, ohne dass Sie eine Referenz auf das Objekt besitzen. Dies wird als "geborgte" Referenz bezeichnet. Wenn Sie eine geborgte Referenz an einen anderen Aufruf übergeben, der erwartet, dass Sie sein Argument INCREF'en (wie oben diskutiert PyList_SetItem()), haben Sie ein Problem.
Ich vermute, dass die Ursache Ihres Problems eine dieser Fälle sein könnte, aber da Sie Ihren Code nicht posten, kann ich hier nicht viel mehr helfen – ich weiß nicht einmal, welche Funktionen Sie aufrufen. Vielleicht könnten Sie eine Liste der aufgerufenen Py*-Funktionen und alle Fragen, die Sie bezüglich ihres Referenzzählungsverhaltens haben, nachschlagen, nachdem Sie sie im Handbuch nachgeschlagen haben?
Natürlich funktionierte das nicht, sonst würde ich mich nicht mit diesem Problem belästigen. Jetzt zerlege ich die Sache in kleinere Funktionen, um zu sehen, ob das hilft, den Fehler zu verfolgen (obwohl es die Funktion fast sicher verlangsamen wird). Gibt es irgendwo eine FAQ zu Referenzzählungs-Problemen?
Es gibt wirklich nichts, was das Verständnis des Referenzzählungsverhaltens jeder verwendeten Funktion ersetzen kann. Das Python/C API-Handbuch ist Ihr Freund. (Und ich verspreche, es zu korrigieren, wenn Sie spezifische Informationen finden, die fehlen oder schwer zu finden sind.)
