Unterschiede zwischen PyPy und CPython¶

Unterschiede in Bezug auf Garbage-Collection-Strategien¶

Die von PyPy verwendeten oder implementierten Garbage-Collectors basieren nicht auf Referenzzählung, so dass die Objekte nicht sofort freigegeben werden, wenn sie nicht mehr erreichbar sind. Die offensichtlichste Auswirkung davon ist, dass Dateien (und Sockets, etc.) nicht sofort geschlossen werden, wenn sie aus dem Gültigkeitsbereich gehen. Bei Dateien, die zum Schreiben geöffnet sind, können Daten für eine Weile in ihren Ausgabepuffern liegen bleiben, wodurch die Datei auf der Festplatte leer oder abgeschnitten erscheint. Außerdem könnten Sie die Obergrenze Ihres Betriebssystems für die Anzahl der gleichzeitig geöffneten Dateien erreichen.

Wenn Sie einen Fall debuggen, in dem eine Datei in Ihrem Programm nicht ordnungsgemäß geschlossen wird, können Sie die -X track-resources Befehlszeilenoption verwenden. Wenn sie angegeben wird, wird für jede Datei und jeden Socket, die der Müllsammler schließt, eine ResourceWarning erzeugt. Die Warnung enthält den Stack-Trace der Position, an der die Datei oder der Socket erstellt wurde, um es einfacher zu machen, zu sehen, welche Teile des Programms Dateien nicht explizit schließen.

Dieser Unterschied zu CPython zu beheben, ist im Wesentlichen unmöglich, ohne einen Ansatz für die Garbage Collection zu erzwingen, bei dem Referenzen gezählt werden. Der Effekt, den man in CPython erhält, ist eindeutig als Nebeneffekt der Implementierung beschrieben worden und nicht als Entscheidung des Sprachdesigns: Programme, die sich darauf verlassen, sind im Grunde genommen falsch. Es wäre eine zu starke Einschränkung, wenn man versuchen würde, das Verhalten von CPython in einer Sprachspezifikation zu erzwingen, da es keine Chance hat, von Jython oder IronPython (oder irgendeiner anderen Portierung von Python nach Java oder NET) übernommen zu werden.

Selbst die naive Idee, eine vollständige GC zu erzwingen, wenn wir uns gefährlich nahe an der Grenze des Betriebssystems befinden, kann in einigen Fällen sehr schlecht sein. Wenn Ihr Programm viele offene Dateien leckt, dann würde es funktionieren, aber einen vollständigen GC-Zyklus für jede n-te geleakte Datei erzwingen. Der Wert von n ist eine Konstante, aber das Programm kann eine beliebige Menge an Speicher benötigen, was einen kompletten GC-Zyklus beliebig lang macht. Das Endergebnis ist, dass PyPy einen willkürlich großen Teil seiner Laufzeit in der GC verbringen würde – was die eigentliche Ausführung verlangsamt, nicht um 10%, nicht um 100%, nicht um 1000%, sondern um einen beliebigen Faktor.

Nach unserem besten Wissen gibt es für dieses Problem keine bessere Lösung als die Korrektur der Programme. Wenn es in Code von Drittanbietern auftritt, bedeutet dies, dass man zu den Autoren gehen und ihnen das Problem erklären muss: Sie müssen ihre geöffneten Dateien schließen, damit sie auf einer nicht auf Python basierenden Implementierung von Python ausgeführt werden können.

Hier sind einige weitere technische Details. Dieses Problem betrifft den genauen Zeitpunkt, zu dem __del__ Methoden aufgerufen werden, was in PyPy (noch in Jython oder IronPython) nicht zuverlässig oder zeitnah ist. Es bedeutet auch, dass schwache Referenzen etwas länger als erwartet am Leben bleiben können. Das macht „weak proxies“ (wie von weakref.proxy() zurückgegeben) etwas weniger nützlich: sie scheinen in PyPy etwas länger am Leben zu bleiben, und plötzlich sind sie wirklich tot und lösen beim nächsten Zugriff ein ReferenceError aus. Jeder Code, der schwache Proxies verwendet, muss solcheReferenceError an jeder Stelle, die sie verwendet, sorgfältig abfangen. (Oder, noch besser, verwendeweakref.proxy() überhaupt nicht; verwende weakref.ref().)

Beachte ein Detail in der Dokumentation für weakref-Callbacks:

Wenn callback angegeben wird und nicht None, und das zurückgegebene weakref-Objekt noch am Leben ist, wird der Callback aufgerufen, wenn das Objekt im Begriff ist, beendet zu werden.

Es gibt Fälle, in denen aufgrund der refcount-Semantik von CPython ein weakref unmittelbar vor oder nach den Objekten stirbt, auf die es verweist (typischerweise mit einem Zirkelverweis). Wenn es zufällig direkt danach stirbt, wird der Callback aufgerufen. In einem ähnlichen Fall in PyPy werden sowohl das Objekt als auch die weakref gleichzeitig als tot betrachtet, und der Callback wird nicht aufgerufen. (Issue #2030)

Es gibt ein paar zusätzliche Implikationen aus dem Unterschied in der GC. Vor allem, wenn ein Objekt eine __del__ hat, wird die __del__ in PyPy nie mehr als einmal aufgerufen; aber CPython wird die gleiche __del__ mehrmals aufrufen, wenn das Objekt wiederbelebt wird und wieder stirbt (zumindest ist es zuverlässig so in älteren CPythons; neuere CPythons versuchen, Destruktoren nicht mehr als einmal aufzurufen, aber es gibt Gegenbeispiele). Die __del__-Methoden werden in der „richtigen“ Reihenfolge aufgerufen, wenn sie auf Objekte zeigen, die aufeinander verweisen, wie in CPython, aber im Gegensatz zu CPython werden ihre __del__-Methoden trotzdem aufgerufen, wenn es einen toten Zyklus von Objekten gibt, die sich gegenseitig referenzieren; CPython würde sie stattdessen in die Liste garbage des gcModuls stellen. Weitere Informationen finden Sie im Blog.

Beachten Sie, dass sich dieser Unterschied in einigen Fällen indirekt bemerkbar machen kann. Zum Beispiel wird ein Generator, der in der Mitte ansteht, in PyPy – wieder – später abgeholt als in CPython. Man kann den Unterschied sehen, wenn das yield-Schlüsselwort, an dem er angehalten wird, selbst in einem try:– oder einem with:-Block eingeschlossen ist. Dies zeigt sich zum Beispiel als Problem 736.

Bei Verwendung der Standard-GC (minimark genannt) funktioniert die eingebaute Funktion id() wie in CPython. Bei anderen GCs gibt sie Zahlen zurück, die keine echten Adressen sind (weil sich ein Objekt mehrmals bewegen kann), und wenn man sie oft aufruft, kann das zu Leistungsproblemen führen.

Bei einer langen Kette von Objekten, von denen jedes einen Verweis auf das nächste hat, und jedes mit einem __del__, wird die GC von PyPy schlecht funktionieren. Positiv ist, dass Benchmarks in den meisten anderen Fällen gezeigt haben, dass die GC von PyPy viel besser funktioniert als die von CPython.

Ein weiterer Unterschied ist, dass, wenn man ein __del__ zu einer existierenden Klasse hinzufügt, diese nicht aufgerufen wird:

>>>> class A(object):.... pass....>>>> A.__del__ = lambda self: None__main__:1: RuntimeWarning: a __del__ method added to an existing type will not be called

Noch obskurer: Dasselbe gilt für Klassen im alten Stil, wenn man das __del__ zu einer Instanz hinzufügt (auch in CPython funktioniert das nicht mit Klassen im neuen Stil). Sie erhalten eine RuntimeWarning in PyPy. Um diese Fälle zu beheben, muss man nur sicherstellen, dass eine __del__-Methode in der Klasse vorhanden ist (auch wenn sie nur pass enthält; sie später zu ersetzen oder zu überschreiben funktioniert problemlos).

Letzter Hinweis: CPython versucht, automatisch ein gc.collect() zu machen, wenn das Programm beendet wird; PyPy nicht. (Es ist sowohl in CPython als auch in PyPy möglich, einen Fall zu entwerfen, in dem mehrere gc.collect() benötigt werden, bevor alle Objekte sterben. Dadurch funktioniert der Ansatz von CPython ohnehin nur „meistens“.)

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.