Różnice związane ze strategiami odśmiecaniaś
Odśmiecacze używane lub zaimplementowane przez PyPy nie są oparte na liczeniu referencji, więc obiekty nie są zwalniane natychmiast, gdy przestają być osiągalne. Najbardziej oczywistym efektem tego jest to, że pliki (i gniazda, itp.) nie są natychmiastowo zamykane, gdy wychodzą poza zakres. Dla plików, które są otwarte do pisania, dane mogą być pozostawione w ich buforach wyjściowych przez jakiś czas, przez co plik na dysku wydaje się pusty lub obcięty. Ponadto, możesz osiągnąć limit systemu operacyjnego na liczbę jednocześnie otwartych plików.
Jeśli debugujesz przypadek, w którym plik w twoim programie nie jest prawidłowo zamknięty, możesz użyć opcji wiersza poleceń -X track-resources
. Jeśli zostanie ona podana, dla każdego pliku i gniazda, które zostaną zamknięte przez Garbage Collector, zostanie wygenerowane ostrzeżenie ResourceWarning
. Ostrzeżenie będzie zawierało ślad stosu pozycji, w której plik lub gniazdo zostało utworzone, aby ułatwić sprawdzenie, które części programu nie zamykają plików w sposób jawny.
Poprawienie tej różnicy w CPythonie jest zasadniczo niemożliwe bez wymuszenia podejścia do zbierania śmieci opartego na zliczaniu preferencji. Efekt, który uzyskujesz w CPython, został wyraźnie opisany jako efekt uboczny implementacji, a nie decyzja projektowa języka: programy polegające na tym są w zasadzie fałszywe. Byłoby zbyt silnym ograniczeniem próbować wymusić zachowanie CPythona w specyfikacji języka, biorąc pod uwagę, że nie ma szans na przyjęcie go przez Jythona lub IronPythona (lub jakikolwiek inny port Pythona do Javy lub.NET).
Nawet naiwny pomysł wymuszania pełnego GC, gdy zbliżamy się niebezpiecznie do limitu systemu operacyjnego, może być bardzo zły w niektórych przypadkach. Jeśli twój program mocno wycieka otwarte pliki, to by działało, ale wymuszaj pełny cykl GC co n’th wyciekły plik. Wartość n jest stała, ale program może zajmować dowolną ilość pamięci, co sprawia, że pełny cykl GC jest arbitralnie długi. W rezultacie PyPy spędziłby arbitralnie dużą część swojego czasu w GC – spowalniając rzeczywiste wykonanie, nie o 10%, 100% czy 1000%, ale o dowolny czynnik.
Według naszej najlepszej wiedzy ten problem nie ma lepszego rozwiązania niż poprawienie programów. Jeśli występuje on w kodzie innych firm, oznacza to udanie się do autorów i wyjaśnienie im problemu: muszą oni zamknąć swoje otwarte pliki, aby można je było uruchomić na dowolnej implementacji Pythona innej niż Python.
Oto kilka szczegółów technicznych. Ten problem wpływa na dokładny czas, w którym __del__
metody są wywoływane, co nie jest niezawodne lub terminowe w PyPy (ani Jython, ani IronPython). Oznacza to również, że słabe referencje mogą pozostać przy życiu nieco dłużej niż oczekiwano. To sprawia, że „słabe referencje” (zwracane przez weakref.proxy()
) stają się nieco mniej użyteczne: w PyPy będą wydawać się żywe przez nieco dłuższy czas, a nagle staną się naprawdę martwe, wywołując ReferenceError
przy następnym dostępie. Każdy kod, który używa słabych proxy, musi starannie wychwytywać takieReferenceError
w każdym miejscu, które ich używa. (Albo, jeszcze lepiej, nie używajweakref.proxy()
w ogóle; użyj weakref.ref()
.)
Uwaga na szczegół w dokumentacji dla wywołań zwrotnych weakref:
If callback is provided and not None, and the returned weakrefobject is still alive, the callback will be called when the objectis about to be finalized.
Istnieją przypadki, gdy z powodu semantyki refcount CPythona, weakref umiera bezpośrednio przed lub po obiektach, na które wskazuje (zazwyczaj z jakimś okrężnym odniesieniem). Jeśli zdarzy się, że umrze tuż po, to wywołanie zwrotne zostanie wywołane. W podobnym przypadku w PyPy, zarówno obiekt jak i weakref będą uważane za martwe w tym samym czasie, a wywołanie zwrotne nie zostanie wywołane. (Issue #2030)
Istnieje kilka dodatkowych implikacji wynikających z różnicy w GC. Przede wszystkim, jeśli obiekt ma __del__
, to __del__
nigdy nie jest wywoływane więcej niż raz w PyPy; ale CPython będzie wywoływał to samo __del__
kilka razy, jeśli obiekt zostanie wskrzeszony i umrze ponownie (przynajmniej tak jest niezawodnie w starszych CPythonach; nowsze CPythony starają się wywoływać destruktory nie więcej niż raz, ale są kontrprzykłady). Metody __del__
są wywoływane we „właściwej” kolejności, jeśli są na obiektach wskazujących na siebie nawzajem, jak w CPythonie, ale w przeciwieństwie do CPythona, jeśli istnieje martwy cykl obiektów odwołujących się do siebie nawzajem, ich metody __del__
są wywoływane tak czy inaczej; CPython zamiast tego umieściłby je na liście garbage
modułu gc
. Więcej informacji jest dostępnych na blogu .
Zauważ, że ta różnica może pokazać się pośrednio w niektórych przypadkach. Forexample, generator pozostawiony w oczekiwaniu w środku jest – ponownie -garbage-collected później w PyPy niż w CPython. Możesz zobaczyć różnicę, jeśli słowo kluczowe yield
, na którym jest zawieszony, jest zamknięte w bloku try:
lub with:
. Pokazuje się to na przykład jako problem 736.
Przy użyciu domyślnego GC (o nazwie minimark
) wbudowana funkcja id()
działa tak samo jak w CPythonie. W przypadku innych GC zwraca ona liczby, które nie są prawdziwymi adresami (ponieważ obiekt może poruszać się kilka razy) i częste jej wywoływanie może prowadzić do problemów z wydajnością.
Zauważ, że jeśli masz długi łańcuch obiektów, każdy z referencją do następnego i każdy z __del__
, GC PyPy będzie działać źle. Po jasnej stronie, w większości innych przypadków, benchmarki pokazały, że GC PyPy’ego działa znacznie lepiej niż GC CPythona.
Inną różnicą jest to, że jeśli dodasz __del__
do istniejącej klasy, nie zostanie ona wywołana:
>>>> class A(object):.... pass....>>>> A.__del__ = lambda self: None__main__:1: RuntimeWarning: a __del__ method added to an existing type will not be called
Jeszcze bardziej niejasne: to samo dotyczy klas w starym stylu, jeśli dołączysz __del__
do instancji (nawet w CPythonie nie działa to z klasami w nowym stylu). W PyPy otrzymasz ostrzeżenie RuntimeWarning. Aby naprawić te przypadki, po prostu upewnij się, że istnieje metoda __del__
w klasie na początek (nawet zawierająca tylko pass
; zastąpienie lub nadpisanie jej później działa dobrze).
Ostatnia uwaga: CPython próbuje wykonać gc.collect()
automatycznie, gdyprogram się kończy; nie PyPy. (Możliwe jest zarówno w CPython, jak i PyPy todesign przypadek, w którym kilka gc.collect()
jest potrzebnych, zanim wszystkie obiektydie. To sprawia, że podejście CPythona i tak działa tylko „przez większość czasu”.)
.