Différences entre PyPy et CPython¶

Différences liées aux stratégies de garbage collection¶

Les garbage collectors utilisés ou implémentés par PyPy ne sont pas basés sur lereference counting, les objets ne sont donc pas libérés instantanément lorsqu’ils ne sont plus atteignables. L’effet le plus évident de ceci est que les fichiers (et les sockets, etc) ne sont pas promptement fermés lorsqu’ils sortent de leur portée. Pour les fichiers qui sont ouverts pour l’écriture, les données peuvent rester dans leur tampon de sortie pendant un certain temps, ce qui fait que le fichier sur le disque semble vide ou tronqué. De plus, vous pourriez atteindre la limite de votre OS sur le nombre de fichiers ouverts simultanément.

Si vous déboguez un cas où un fichier de votre programme n’est pas ferméproctement, vous pouvez utiliser l’option de ligne de commande -X track-resources. Si elle estdonnée, un ResourceWarning est produit pour chaque fichier et socket que legarbage collector ferme. L’avertissement contiendra la trace de la pile de la position où le fichier ou le socket a été créé, pour qu’il soit plus facile de voir quelles parties du programme ne ferment pas les fichiers explicitement.

Corriger cette différence à CPython est essentiellement impossible sans forcer une approche de comptage des références pour le garbage collector. L’effet que vous obtenez dans CPython a clairement été décrit comme un effet secondaire de l’implémentation et non une décision de conception du langage : les programmes qui s’appuient sur cela sont fondamentalement bidons. Ce serait une restriction trop forte d’essayer d’imposer le comportement de CPython dans une spécification de langage, étant donné qu’il n’a aucune chance d’être adopté par Jython ou IronPython (ou tout autre portage de Python vers Java ou .NET).

Même l’idée naïve de forcer une GC complète lorsque nous nous approchons dangereusement de la limite du système d’exploitation peut être très mauvaise dans certains cas. Si votre programme fuit des fichiers ouverts de manière importante, alors cela fonctionnerait, mais forcerait un cycle GC complet chaque nième fichier fui. La valeur de n est une constante, mais le programme peut prendre une quantité arbitraire de mémoire, ce qui rend un cycle GC complet arbitrairement long. Le résultat final est que PyPy passerait une fraction arbitrairement grande de son temps d’exécution dans le GC – ralentissant l’exécution réelle, non pas de 10% ni de 100% ni de 1000% mais par essentiellement n’importe quel facteur.

A notre connaissance, ce problème n’a pas de meilleure solution que de corriger les programmes. S’il se produit dans du code tiers, cela signifie qu’il faut aller voir les auteurs et leur expliquer le problème : ils doivent fermer leurs fichiers ouverts afin de pouvoir fonctionner sur toute implémentation de Python non basée sur Python.

Voici quelques détails plus techniques. Ce problème affecte le moment précis auquel les méthodes __del__ sont appelées, ce qui n’est pas fiable ou opportun dans PyPy (ni Jython ni IronPython). Cela signifie également que les références faibles peuvent rester en vie un peu plus longtemps que prévu. Cela rend les « proxies faibles » (tels que retournés par weakref.proxy()) un peu moins utiles : ils sembleront rester en vie un peu plus longtemps dans PyPy, et soudainement ils seront vraiment morts, soulevant un ReferenceError lors du prochain accès. Tout code qui utilise des proxies faibles doit soigneusement attraper de tels ReferenceError à tout endroit qui les utilise. (Ou, mieux encore, ne pas utiliserweakref.proxy() du tout ; utiliser weakref.ref().)

Notez un détail dans la documentation pour les callbacks weakref:

Si callback est fourni et pas None, et que l’objet weakref retourné est toujours vivant, le callback sera appelé lorsque l’objet sera sur le point d’être finalisé.

Il y a des cas où, à cause de la sémantique refcount de CPython, une weakref meurt immédiatement avant ou après les objets qu’elle pointe (typiquement avec une certaine référence circulaire). Si elle meurt juste après, alors le callback sera invoqué. Dans un cas similaire dans PyPy, l’objet et la weakref seront tous deux considérés comme morts en même temps,et le callback ne sera pas invoqué. (Issue #2030)

Il y a quelques implications supplémentaires de la différence dans le GC. Notamment, si un objet a un __del__, le __del__ n’est jamais appelé plus d’une fois dans PyPy ; mais CPython appellera le même __del__ plusieurs fois si l’objet est ressuscité et meurt à nouveau (au moins c’est ainsi de manière fiable dans les anciens CPython ; les CPython plus récents essaient d’appeler les destructeurs pas plus d’une fois, mais il y a des contre-exemples). Les méthodes __del__ sont appelées dans le « bon » ordre si elles sont sur des objets pointant les uns vers les autres, comme dans CPython, mais contrairement à CPython, s’il y a un cycle mort d’objets se référant les uns aux autres, leurs méthodes __del__ sont appelées de toute façon ; CPython les mettrait plutôt dans la liste garbage du module gc. Plus d’informations sont disponibles sur le blog .

Notez que cette différence pourrait se manifester indirectement dans certains cas. Forexample, un générateur laissé en attente au milieu est – encore une fois – collecté plus tard dans PyPy que dans CPython. Vous pouvez voir la différence si le mot-clé yield auquel il est suspendu est lui-même enfermé dans un bloc try: ou with:. Cela apparaît par exemple comme le problème 736.

En utilisant le GC par défaut (appelé minimark), la fonction intégrée id() fonctionne comme dans CPython. Avec d’autres GC, elle renvoie des nombres qui ne sont pas des adresses réelles (parce qu’un objet peut se déplacer plusieurs fois)et l’appeler beaucoup peut conduire à un problème de performance.

Notez que si vous avez une longue chaîne d’objets, chacun avec une référence au suivant, et chacun avec un __del__, la GC de PyPy aura de mauvaises performances. Du bon côté, dans la plupart des autres cas, les benchmarks ont montré que les GC de PyPy sont bien plus performants que ceux de CPython.

Une autre différence est que si vous ajoutez un __del__ à une classe existante, elle ne sera pas appelée :

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

Encore plus obscur : il en va de même, pour les classes de style ancien, si vous attachez le __del__ à une instance (même dans CPython, cela ne fonctionne pas avec les classes de style nouveau). Vous obtenez un RuntimeWarning dans PyPy. Pour résoudre ces cas, assurez-vous simplement qu’il y a une méthode __del__ dans la classe pour commencer (même contenant seulement pass ; la remplacer ou la surcharger plus tard fonctionne bien).

Dernière note : CPython essaie de faire un gc.collect() automatiquement lorsque leprogramme se termine ; pas PyPy. (Il est possible, tant dans CPython que dans PyPy, de concevoir un cas où plusieurs gc.collect() sont nécessaires avant que tous les objets ne meurent. Cela fait que l’approche de CPython ne fonctionne que « la plupart du temps » de toute façon.)

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.