Accélerer la vitesse de vos scripts Python à l’aide de PyPy
- 2021-06-22
- Publié par : Christophe DELEUZE
- Catégorie : Python
Python est un langage interprété. Cela signifie qu’il a besoin d’un interpréteur pour être exécuté. Le rôle de l’interpréteur est de lire chaque ligne d’un code et de les traduire en langage machine qui sera alors exécuté à l’aide d’une machine virtuelle. Toutefois, le principal défaut d’un interpréteur est sa lenteur. Parmi les alternatives pour corriger ce défaut, une se démarque du lot en utilisant la compilation à la volée ( JIT – Just In Time Compiler) : PyPy.
De l'interpréteur au langage machine
Dans les faits, nos ordinateurs ne savent pas exécuter du code qui est écrit en langage Python directement. Par contre, ils savent le faire avec le langage machine. En python, la traduction d’un code vers le langage machine se fait à l’aide d’un interpréteur. Cet interpréteur procède en deux étapes pour exécuter vos scripts. En premier lieu, le code Python est converti (compilé) en bytecode qui est une représentation binaire de celui-ci. Puis, celui-ci est exécuté par une machine virtuelle (pas directement par le processeur).
La machine virtuelle convertit les instructions binaires en instructions machines spécifiques. En Python, on dit que l’implémentation du langage est fait à l’aide de CPython. CPython (logiciel libre) est un interpréteur de bytecode écrit en langage C.
Résumé en schéma :
Python a acquis la réputation d’être puissant, flexible et facile à utiliser. Ces vertus ont conduit à son utilisation dans une grande variété d’applications, de flux de travail et de domaines. Mais la conception du langage, sa nature interprétée et son dynamisme d’exécution signifie que Python a toujours été un ordre de grandeur plus lent que les langages natifs comme le C, le Pascal, le C++ et le Rust.
Au fil des ans, les développeurs ont proposé plusieurs solutions de contournement pour contourner les limitations de vitesse de Python. Par exemple, vous pouvez écrire des tâches exigeantes en performances en C et les envelopper (wrapper) avec Python ; de nombreuses bibliothèques d’apprentissage automatique font exactement cela. Ou vous pouvez utiliser Cython, un projet qui vous permet de saupoudrer le code Python d’informations de type runtime qui permettent de le compiler en C.
Mais les solutions de contournement ne sont jamais idéales. Alors, des développeurs ont développé d’autres implémentations du langage Python afin de répondre à cette problématique de vitesse. Celle qui nous intéresse est évidemment PyPy, qui utilise un système de compilation à la volée : Just-In-Time compiler (JIT compiler).
La compilation à la volée : Just In Time Compiler (JIT Compiler)
La compilation à la volée est une technique visant à améliorer la performance de systèmes bytecode-compilés par la traduction de bytecode en code machine natif au moment de l’exécution.
La compilation au sens classique du terme, qui produit le bytecode, n’est que la première étape du processus d’exécution. Ensuite, le bytecode est déployé sur le système cible, et lorsque ce bytecode est exécuté, le compilateur du langage de programmation, à la volée, le traduit en code machine natif. Ceci peut être fait sur un fichier entier, ou spécifiquement sur une fonction du programme.
Le but de cette démarche est de combiner les avantages de la compilation native et du bytecode. Comme le bytecode déployé est portable, la compilation à la volée est envisageable pour tout type d’architecture. La seule condition à respecter est d’avoir un compilateur JIT pour chaque architecture utilisée. Toutefois, cela est facilité par le fait que les compilateurs de bytecode en code machine sont plus faciles à écrire que les compilateurs code source – code natif.
Pour vous aider à y voir plus clair, voici un tableau résumant les grandes différences entre les différents types de langages :
Cette illustration devrait vous aider à comprendre la différence. Dans les langages compilés (C, Pascal, C++, Rust) la phase de compilation est strictement partitionnée à l’environnement de développement. Il produit un binaire exécutable, qui est ensuite envoyé en production. En interprété, c’est tout le contraire qui est vrai : le code source est poussé dans son intégralité vers la production, où un interpréteur l’exécutera. Enfin, les langages JIT contiennent également du code source (ou du bytecode), mais ils sont compilés et exécutés comme un langage compilé normal plutôt qu’interprété ligne par ligne.
Retour vers PyPy
Si vous souhaitez utiliser PyPy, celui-ci viendra remplacer directement l’interpréteur CPython d’origine.
Selon la tâche exécutée, les gains de performance peuvent être spectaculaires. En moyenne, PyPy accélère Python d’environ 7,6 fois, certaines tâches étant accélérées 50 fois ou plus. L’interpréteur CPython n’effectue tout simplement pas les mêmes types d’optimisations que PyPy, et ne le fera probablement jamais, car ce n’est pas l’un de ses objectifs de conception.
Le plus intéressant avec PyPy c’est que son utilisation ne requerra pas ou peu d’effort de la part du développeur pour débloquer les gains de performance. Remplacez uniquement CPython par PyPy et dans la majorité des cas, vous aurez terminé.
Il existe quelques exceptions, mais l’objectif déclaré de PyPy est d’exécuter du code Python existant et non modifié afin de lui fournir une accélération automatique.
Compatibilité de PyPy
PyPy prend actuellement en charge Python 2.x et Python 3.x. Vous devrez donc télécharger différentes versions de PyPy en fonction de la version de Python que vous exécuterez.
En plus de prendre en charge l’ensemble du langage Python de base, PyPy fonctionne avec la grande majorité des outils de l’écosystème Python, tels que pip pour la gestion des librairies ou virtualenv pour les environnements virtuels. La plupart des packages Python, même ceux avec des modules C, devraient fonctionner tels quels, bien qu’il existe des limitations que nous aborderons ci-dessous.
PyPy s’écarte également de la façon dont CPython gère certaines fonctions internes, mais essaie de préserver les comportements compatibles. Par exemple, PyPy gère le ramasse-miettes différemment de CPython. Tous les objets ne sont pas immédiatement collectés une fois qu’ils sont hors de portée, de sorte qu’un programme Python exécuté sous PyPy peut afficher une empreinte mémoire plus importante que lorsqu’il est exécuté sous CPython. Mais vous pouvez toujours utiliser les contrôles de récupération de place de haut niveau de Python exposés via le module gc, tels que gc.enable(), gc.disable() et gc.collect().
Optimiser votre code pour PyPy
Si vous souhaitez des informations sur le comportement JIT de PyPy au moment de l’exécution, PyPy inclut un module, pypyjit, qui expose de nombreux hooks JIT à votre application Python. Si vous avez une fonction ou un module qui semble mal fonctionner avec le JIT, pypyjit vous permettra d’obtenir des statistiques détaillées à son sujet.
Un autre module spécifique à PyPy : __pypy__, expose d’autres fonctionnalités spécifiques à PyPy, et peut donc être utile pour écrire des applications qui exploitent ces fonctionnalités. De plus, en raison du dynamisme d’exécution de Python, il est possible de construire des applications Python qui utilisent ces fonctionnalités lorsque PyPy est présent et les ignore lorsqu’il ne l’est pas.
PyPy fonctionne mieux avec des applications Python pures
PyPy a toujours mieux fonctionné avec des applications Python pures, c’est-à-dire des applications écrites en Python et rien d’autre. Les packages Python qui s’interfacent avec les bibliothèques C, telles que NumPy, ne se sont pas aussi bien supportés en raison de la façon dont PyPy émule les interfaces binaires natives de CPython.
Les développeurs de PyPy ont réduit ce problème et ont rendu PyPy plus compatible avec la majorité des packages Python qui dépendent des extensions C. Numpy, par exemple, fonctionne très bien avec PyPy maintenant. Mais si vous voulez une compatibilité maximale avec les extensions C, utilisez CPython.
PyPy fonctionne mieux avec des applications de longue durée
Au regard des optimisations effectuées par PyPy sur des programmes Python, on constate que les programmes de longue durée bénéficient le plus de ses optimisations. Plus le programme s’exécute longtemps, plus PyPy peut collecter d’informations sur le type d’exécution et plus, il peut effectuer d’optimisations. Les scripts Python à utilisation unique ne bénéficieront pas de ce genre de gain. Les applications qui en bénéficient ont généralement des boucles qui s’exécutent pendant de longues périodes où s’exécutent en continu en arrière-plan, par exemple des frameworks Web (Django).
PyPy ne fait pas de compilation à l'avance
Enfin, à propos de la compilation, PyPy compile le code Python, mais ce n’est pas un compilateur pour le code Python. En raison de la façon dont PyPy effectue ses optimisations et du dynamisme inhérent de Python, il n’y a aucun moyen d’émettre le code JITted résultant en tant que binaire autonome et de le réutiliser. Chaque programme doit être compilé pour chaque exécution. Si vous souhaitez compiler Python en un code plus rapide pouvant s’exécuter en tant qu’application autonome, utilisez Cython, Numba ou Nuitka (expérimental).
En avant vers le test
Avant le mot de la fin, je ne peux pas me permettre de vous laisser partir sans vous donner une petite ligne de code pour tester par vous-même PyPy.
Aussi, pas de prise de tête, avec Conda vous pourrez créer un environnement PyPy dédié comme s’il s’agissait d’un environnement standard CPython :
conda create -n pypy3 -c conda-forge pypy3.7
Pour le reste, à vous de jouer !
Le mot de la fin
Aussi magique que puisse paraître PyPy, ce n’est pas de la magie. PyPy a certaines limitations qui réduisent ou annulent son efficacité pour certains types de programmes. Hélas, PyPy n’est pas un remplacement complètement universel de CPython mais il reste très prometteur et pourrait vous servir dans de nombreuses situations. D’ailleurs, je vous invite sincèrement à explorer ce que vous pouvez en faire en fonction de votre contexte. Pour le reste, les remarques, les critiques, les commentaires sont là pour vous lâcher.
Merci pour vos explications , c’était claire et utile