Diferențe între PyPy și CPython¶

Diferențe legate de strategiile de colectare a gunoiului¶

Colectoarele de gunoi utilizate sau implementate de PyPy nu se bazează pe numărarea referințelor, astfel încât obiectele nu sunt eliberate instantaneu atunci când nu mai sunt accesibile. Efectul cel mai evident al acestui lucru este că fișierele (și socket-urile, etc.) nu sunt închise prompt atunci când ies din domeniul de aplicare. În cazul fișierelor care sunt deschise pentru scriere, datele pot fi lăsate pentru o perioadă de timp în tampoanele de ieșire, ceea ce face ca fișierul de pe disc să apară gol sau trunchiat. În plus, s-ar putea să atingeți limita OS-ului dumneavoastră privind numărul de fișiere deschise simultan.

Dacă depanați un caz în care un fișier din programul dumneavoastră nu este închis în mod corespunzător, puteți utiliza opțiunea de linie de comandă -X track-resources. Dacă aceasta este dată, se produce un ResourceWarning pentru fiecare fișier și socket pe care colectorul de gunoi îl închide. Avertismentul va conține urma de stivă a poziției în care a fost creat fișierul sau socket-ul, pentru a face mai ușor de văzut care părți ale programului nu închid fișierele în mod explicit.

Repararea acestei diferențe la CPython este în esență imposibilă fără a forța o abordare de numărare a referințelor pentru colectarea gunoiului. Efectul pe care îl obțineți în CPython a fost descris în mod clar ca fiind un efect secundar al implementării și nu o decizie de proiectare a limbajului: programele care se bazează pe acest lucru sunt practic false. Ar fi o restricție prea puternică să încercăm să impunem comportamentul luiCPython într-o specificație de limbaj, având în vedere că nu are nici o șansă să fieadoptat de Jython sau IronPython (sau orice alt port al Python în Java sau.NET).

Chiar și ideea naivă de a forța o GC completă atunci când ne apropiem periculos de mult de limita sistemului de operare poate fi foarte proastă în unele cazuri. Dacă programul dvs. scurge foarte mult fișierele deschise, atunci ar funcționa, dar forțați un ciclu GC complet la fiecare al n-lea fișier scurs. Valoarea lui n este o constantă, dar programul poate ocupa o cantitate arbitrară de memorie, ceea ce face ca un ciclu GC complet să fie arbitrar de lung. Rezultatul final este că PyPy ar petrece o fracțiune arbitrar de mare din timpul de execuție în GC – încetinind execuția propriu-zisă, nu cu 10%, nici cu 100%, nici cu 1000%, ci cu practic orice factor.

Din câte știm noi, această problemă nu are o soluție mai bună decât fixarea programelor. Dacă apare în coduri de la terți, acest lucru înseamnă să mergem la autori și să le explicăm problema: trebuie să își închidă fișierele deschise pentru a putea rula pe orice implementare de Python care nu este bazată pe PCython.

Iată câteva detalii tehnice suplimentare. Această problemă afectează momentul precis în care sunt apelate metodele __del__, care nu este fiabil sau oportun în PyPy (nici în Jython și nici în IronPython). Aceasta înseamnă, de asemenea, că referințele slabe pot rămâne în viață un pic mai mult decât se așteaptă. Acest lucru face ca „proxy-urile slabe” (așa cum sunt returnate de weakref.proxy()) să fie ceva mai puțin utile: acestea vor părea să rămână în viață pentru ceva mai mult timp în PyPy și, dintr-o dată, vor fi cu adevărat moarte, ridicând un ReferenceError la următoarea accesare. Orice cod care utilizează proxy-uri slabe trebuie să prindă cu atenție astfel de ReferenceError în orice loc care le folosește. (Sau, mai bine, nu folosiți delocweakref.proxy(); folosiți weakref.ref().)

Rețineți un detaliu din documentația pentru callback-urile weakref:

Dacă callback-ul este furnizat și nu None, iar obiectul weakref returnat este încă în viață, callback-ul va fi apelat atunci când obiectul este pe cale să fie finalizat.

Există cazuri în care, din cauza semanticii refcount a CPython, un weakrefdies imediat înainte sau după obiectele pe care le indică (de obicei cu o referință circulară). Dacă se întâmplă să moară imediat după, atunci se va invoca callback-ul. Într-un caz similar în PyPy, atât obiectul, cât și referința slabă vor fi considerate moarte în același timp, iar callback-ul nu va fi invocat. (Problema nr. 2030)

Există câteva implicații suplimentare din cauza diferenței din GC. Cel mai important, dacă un obiect are un __del__, __del__ nu este niciodată apelat mai mult de o dată în PyPy; dar CPython va apela același __del__ de mai multe ori dacă obiectul este resuscitat și moare din nou (cel puțin așa este în mod fiabil în CPythons mai vechi; CPythons mai noi încearcă să apeleze destructori nu mai mult de o dată, dar există contra-exemple). Metodele __del__ sunt apelate în ordinea „corectă” dacă sunt pe obiecte care se referă unul la altul, ca în CPython, dar, spre deosebire de CPython, dacă există un ciclu mort de obiecte care se referă unul la altul, metodele lor __del__ sunt oricum apelate; CPython le-ar pune în schimb în lista garbage a modulului gc. Mai multe informații sunt disponibile pe blogul .

Rețineți că această diferență ar putea apărea indirect în unele cazuri. Forexemplu, un generator lăsat în așteptare la mijloc este – din nou -colectat mai târziu în PyPy decât în CPython. Puteți vedeadiferența dacă cuvântul cheie yield la care este suspendat este el însușiînchis într-un bloc try: sau with:. Acest lucru apare, de exemplu, ca problema 736.

Utilizând GC implicit (numit minimark), funcția încorporată id()funcționează ca și în CPython. Cu alte GC-uri, aceasta returnează numere care nu sunt adrese reale (deoarece un obiect se poate deplasa de mai multe ori)și apelarea ei de multe ori poate duce la probleme de performanță.

Rețineți că, dacă aveți un lanț lung de obiecte, fiecare cu o referință la următorul și fiecare cu un __del__, GC-ul PyPy va avea performanțe slabe. Partea bună a lucrurilor este că, în majoritatea celorlalte cazuri, testele de referință au arătat că GC-ul lui PyPy se comportă mult mai bine decât cel al lui CPython.

O altă diferență este că, dacă adăugați un __del__ la o clasă existentă, aceasta nu va fi apelată:

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

Chiar și mai obscur: același lucru este valabil, pentru clasele de tip vechi, dacă atașați __del__ la o instanță (chiar și în CPython acest lucru nu funcționează cu clasele de tip nou). Primiți un RuntimeWarning în PyPy. Pentru a rezolva aceste cazuri, asigurați-vă că există o metodă __del__ în clasă pentru început (chiar dacă conține doar pass; înlocuirea sau suprascrierea ei mai târziu funcționează bine).

Ultima notă: CPython încearcă să facă un gc.collect() automat când programul se termină; nu și PyPy. (Este posibil atât în CPython cât și în PyPy să se proiecteze un caz în care sunt necesare mai multe gc.collect() înainte ca toate obiectele să moară. Acest lucru face ca abordarea CPython să funcționeze oricum doar „de cele mai multe ori”)

.

Lasă un răspuns

Adresa ta de email nu va fi publicată.