Python est lent. Pourquoi?

Récemment, on peut observer la popularité croissante du langage de programmation Python. Il est utilisé en DevOps, en analyse de données, en développement web, dans le domaine de la sécurité et dans d'autres domaines. Mais voici la vitesse ... Il n'y a rien à se vanter de cette langue ici. L'auteur du matériel, dont nous publions la traduction aujourd'hui, a décidé de découvrir les raisons de la lenteur de Python et de trouver les moyens de l'accélérer.



Dispositions générales


Comment Java, en termes de performances, est-il lié au C ou au C ++? Comment comparer C # et Python? Les réponses à ces questions dépendent fortement du type de demandes analysées par le chercheur. Il n'y a pas de référence parfaite, mais en étudiant les performances de programmes écrits dans différentes langues, The Computer Language Benchmarks Game peut être un bon point de départ .

Je fais référence au jeu de référence en langage informatique depuis plus de dix ans. Python, en comparaison avec d'autres langages, tels que Java, C #, Go, JavaScript, C ++, est l'un des plus lents . Cela inclut les langages qui utilisent la compilation JIT (C #, Java) et la compilation AOT (C #, C ++), ainsi que les langages interprétés tels que JavaScript.

Ici, je voudrais noter que lorsque je dis «Python», je veux dire l'implémentation de référence de l'interpréteur Python - CPython. Dans ce document, nous aborderons ses autres implémentations. En fait, ici, je veux trouver la réponse à la question de savoir pourquoi Python prend 2 à 10 fois plus de temps que d'autres langages pour résoudre des problèmes comparables, et si cela peut être fait plus rapidement.

Voici quelques théories de base essayant d'expliquer pourquoi Python est lent:

  • La raison en est le GIL (Global Interpreter Lock, Global Interpreter Lock).
  • La raison en est que Python est un langage interprété plutôt que compilé.
  • La raison en est le typage dynamique.

Nous allons analyser ces idées et essayer de trouver la réponse à la question de savoir ce qui a le plus d'effet sur les performances des applications Python.

Gil


Les ordinateurs modernes ont des processeurs multicœurs et des systèmes multiprocesseurs sont parfois trouvés. Afin d'utiliser toute cette puissance de calcul, le système d'exploitation utilise des structures de bas niveau appelées threads, tandis que les processus (par exemple, le processus du navigateur Chrome) peuvent lancer de nombreux threads et les utiliser en conséquence. Par conséquent, par exemple, si un processus a particulièrement besoin de ressources processeur, son exécution peut être divisée en plusieurs cœurs, ce qui permet à la plupart des applications de résoudre plus rapidement les tâches auxquelles elles sont confrontées.

Par exemple, mon navigateur Chrome, au moment où j'écris ceci, a 44 discussions ouvertes. Il convient de garder à l'esprit que la structure et l'API du système pour travailler avec les flux varient dans les systèmes d'exploitation Posix (Mac OS, Linux) et dans la famille de systèmes d'exploitation Windows. Le système d'exploitation prévoit également des threads.

Si vous n'avez jamais rencontré de programmation multithread auparavant, vous devez maintenant vous familiariser avec les soi-disant verrous (locks). La signification des verrous est qu'ils vous permettent d'assurer un tel comportement du système lorsque, dans un environnement multithread, par exemple, lors du changement d'une certaine variable en mémoire, plusieurs threads ne peuvent pas accéder à la même zone mémoire (pour la lecture ou la modification).

Lorsque l'interpréteur CPython crée les variables, il alloue de la mémoire, puis compte le nombre de références existantes à ces variables. Ce concept est connu sous le nom de comptage de références. Si le nombre de liens est égal à zéro, alors la mémoire correspondante est libérée. C'est pourquoi, par exemple, la création de variables "temporaires", disons, dans le cadre de boucles, n'entraîne pas une augmentation excessive de la quantité de mémoire consommée par l'application.

La partie la plus intéressante commence lorsque plusieurs threads partagent les mêmes variables, et le problème principal ici est de savoir exactement comment CPython effectue le comptage des références. C'est là que l'action du «verrou d'interprète global» apparaît, qui contrôle soigneusement l'exécution des threads.

Un interprète ne peut effectuer qu'une seule opération à la fois, quel que soit le nombre de threads dans le programme.

OwComment GIL affecte-t-il les performances des applications Python?


Si nous avons une application monothread s'exécutant dans le même processus d'interpréteur Python, alors le GIL n'affecte en rien les performances. Si, par exemple, vous vous débarrassez de GIL, nous ne remarquerons aucune différence de performance.

Si, dans le cadre d'un processus d'interpréteur Python, il est nécessaire d'implémenter un traitement de données parallèle à l'aide de mécanismes multithreading, et que les flux utilisés utiliseront intensivement le sous-système d'E / S (par exemple, s'ils fonctionnent avec un réseau ou avec un disque), alors il sera possible d'observer les conséquences de comment GIL gère les threads. Voici à quoi cela ressemble dans le cas de l'utilisation de deux threads, avec des processus de chargement intensifs.


Visualisation GIL (prise à partir d'ici )

Si vous avez une application Web (par exemple, basée sur le framework Django) et que vous utilisez WSGI, alors chaque demande pour l'application Web sera traitée par un processus d'interpréteur Python distinct, c'est-à-dire que nous n'avons qu'un seul verrou de demande. Étant donné que l'interpréteur Python démarre lentement, dans certaines implémentations WSGI, il existe un soi-disant «mode démon», lors de l'utilisation duquel les processus de l'interpréteur sont maintenus en état de fonctionnement, ce qui permet au système de traiter les demandes plus rapidement.

OwComment les autres interprètes Python se comportent-ils?


PyPy a un GIL, il est généralement plus de 3 fois plus rapide que CPython.

Il n'y a pas de GIL en Jython, car les threads Python en Jython sont représentés comme des threads Java. Ces threads utilisent les capacités de gestion de la mémoire de la JVM.

OwComment le contrôle de flux est-il organisé en JavaScript?


Si nous parlons de JavaScript, alors, tout d'abord, il convient de noter que tous les moteurs JS utilisent l'algorithme de collecte des ordures de marquage et de balayage . Comme déjà mentionné, la principale raison d'utiliser GIL est l'algorithme de gestion de la mémoire utilisé dans CPython.

JavaScript n'a pas de GIL, cependant, JS est un langage à thread unique, par conséquent, il n'a pas besoin d'un tel mécanisme. Au lieu de l'exécution de code parallèle, JavaScript utilise des techniques de programmation asynchrones basées sur une boucle d'événements, des promesses et des rappels. Python a quelque chose de similaire fourni par le module asyncio .

Python - langage interprété


J'ai souvent entendu que les mauvaises performances de Python sont dues au fait qu'il s'agit d'un langage interprété. Ces déclarations sont basées sur une simplification grossière du fonctionnement réel de CPython. Si, dans le terminal, vous entrez une commande comme python myscript.py , alors CPython commencera une longue séquence d'actions, qui consiste en la lecture, l'analyse lexicale, l'analyse, la compilation, l'interprétation et l'exécution du code de script. Si vous êtes intéressé par les détails, jetez un œil à ce matériel.

Pour nous, lors de l'examen de ce processus, il est particulièrement important qu'ici, au stade de la compilation, un fichier .pyc soit créé et qu'une séquence de bytecodes soit écrite dans le fichier dans le __pycache__/ , qui est utilisé à la fois dans Python 3 et Python. 2.

Cela s'applique non seulement aux scripts que nous avons écrits, mais également au code importé, y compris les modules tiers.

Par conséquent, la plupart du temps (sauf si vous écrivez du code qui ne s'exécute qu'une seule fois), Python exécutera le bytecode terminé. En comparant cela avec ce qui se passe en Java et C #, il s'avère que le code Java est compilé dans le «langage intermédiaire», et la machine virtuelle Java lit le bytecode et effectue sa compilation JIT en code machine. Le «langage intermédiaire» .NET CIL (qui est le même que le .NET Common-Language-Runtime, CLR) utilise la compilation JIT pour naviguer vers le code machine.

En conséquence, en Java et en C #, un «langage intermédiaire» est utilisé et des mécanismes similaires sont présents. Pourquoi, alors, Python affiche-t-il des repères bien pires que Java et C # si tous ces langages utilisent des machines virtuelles et une sorte de bytecode? Tout d'abord, du fait que la compilation JIT est utilisée en .NET et Java.

La compilation JIT (compilation Just In Time, compilation à la volée ou juste à temps) nécessite un langage intermédiaire afin de permettre la division du code en fragments (frames). Les systèmes de compilation AOT (compilation Ahead Of Time, compilation avant exécution) sont conçus de manière à assurer la pleine fonctionnalité du code avant que l'interaction de ce code avec le système ne commence.

En soi, l'utilisation de JIT n'accélère pas l'exécution du code, car certains fragments de code octet entrent en exécution, comme en Python. Cependant, JIT vous permet d'effectuer des optimisations de code pendant l'exécution. Un bon optimiseur JIT est capable d'identifier les parties les plus chargées de l'application (cette partie de l'application est appelée le «point chaud») et d'optimiser les fragments de code correspondants, en les remplaçant par des options optimisées et plus productives que celles qui étaient utilisées précédemment.

Cela signifie que lorsqu'une certaine application exécute certaines actions encore et encore, une telle optimisation peut accélérer considérablement l'exécution de ces actions. Gardez également à l'esprit que Java et C # sont des langages fortement typés, de sorte que l'optimiseur peut émettre plus d'hypothèses sur le code qui peuvent aider à améliorer les performances du programme.

Il existe un compilateur JIT dans PyPy, et, comme déjà mentionné, cette implémentation de l'interpréteur Python est beaucoup plus rapide que CPython. Des informations sur la comparaison des différents interprètes Python peuvent être trouvées dans cet article.

▍ Pourquoi CPython n'utilise pas de compilateur JIT?


Les compilateurs JIT présentent également des inconvénients. L'un d'eux est l'heure de lancement. CPython démarre déjà relativement lentement et PyPy est 2 à 3 fois plus lent que CPython. Le long terme de la JVM est également un fait connu. CLR .NET contourne ce problème en démarrant lors du démarrage du système, mais il convient de noter que le CLR et le système d'exploitation qui exécute le CLR sont développés par la même société.

Si vous avez un processus Python en cours d'exécution depuis longtemps, alors que dans un tel processus il y a du code qui peut être optimisé, car il contient des sections très utilisées, alors vous devriez sérieusement regarder un interpréteur qui a un compilateur JIT.

Cependant, CPython est une implémentation de l'interpréteur Python à usage général. Par conséquent, si vous développez, à l'aide de Python, une application en ligne de commande, la nécessité d'une longue attente pour que le compilateur JIT démarre à chaque lancement de cette application ralentira considérablement le travail.

CPython essaie de prendre en charge autant de cas d'utilisation Python que possible. Par exemple, il est possible de connecter le compilateur JIT à Python, cependant, le projet qui implémente cette idée ne se développe pas très activement.

Par conséquent, nous pouvons dire que si vous utilisez Python pour écrire un programme dont les performances peuvent s'améliorer lors de l'utilisation du compilateur JIT, utilisez l'interpréteur PyPy.

Python est un langage typé dynamiquement


Dans les langages typés statiquement, lors de la déclaration de variables, vous devez spécifier leurs types. Parmi ces langages, on peut noter C, C ++, Java, C #, Go.

Dans les langages typés dynamiquement, le concept d'un type de données a la même signification, mais le type d'une variable est dynamique.

 a = 1 a = "foo" 

Dans cet exemple le plus simple, Python crée d'abord la première variable a , puis la seconde avec le même nom de type str , et libère la mémoire allouée à la première variable a .

Il peut sembler que l'écriture dans des langues avec une frappe dynamique est plus pratique et plus simple que dans des langues avec une frappe statique, cependant, ces langues n'ont pas été créées sur le caprice de quelqu'un. Au cours de leur développement, les caractéristiques des systèmes informatiques ont été prises en compte. Tout ce qui est écrit dans le texte du programme se résume finalement aux instructions du processeur. Cela signifie que les données utilisées par le programme, par exemple sous la forme d'objets ou d'autres types de données, sont également converties en structures de bas niveau.

Python effectue de telles transformations automatiquement, le programmeur ne voit pas ces processus et il n'a pas besoin de s'occuper de ces transformations.

Ne pas avoir à spécifier le type d'une variable lors de sa déclaration n'est pas une caractéristique du langage qui ralentit Python. L'architecture du langage permet de rendre presque n'importe quoi dynamique. Par exemple, au moment de l'exécution, vous pouvez remplacer les méthodes d'objet. Encore une fois, pendant l'exécution du programme, vous pouvez utiliser la technique du «patch de singe» appliquée aux appels système de bas niveau. En Python, presque tout est possible.

C'est l'architecture Python qui rend l'optimisation extrêmement difficile.

Afin d'illustrer cette idée, je vais utiliser un outil de traçage des appels système sur MacOS appelé DTrace.

Il n'y a pas de mécanismes de prise en charge DTrace dans la distribution CPython finie, donc CPython devra être recompilé avec les paramètres appropriés. Ici, la version 3.6.6 est utilisée. Nous utilisons donc la séquence d'actions suivante:

 wget https://github.com/python/cpython/archive/v3.6.6.zip unzip v3.6.6.zip cd v3.6.6 ./configure --with-dtrace make 

Maintenant, à l'aide de python.exe , vous pouvez utiliser DTRace pour tracer le code. Lisez à propos de l'utilisation de DTrace avec Python ici . Et ici, vous pouvez trouver des scripts pour mesurer divers indicateurs de performance des programmes Python à l'aide de DTrace. Parmi eux figurent des paramètres pour appeler des fonctions, le temps d'exécution des programmes, le temps d'utilisation du processeur, des informations sur les appels système, etc. Voici comment utiliser la commande dtrace :

 sudo dtrace -s toolkit/<tracer>.d -c '../cpython/python.exe script.py' 

Et voici comment la py_callflow trace py_callflow affiche les appels de fonction dans l'application.


Suivi à l'aide de DTrace

Répondons maintenant à la question de savoir si le typage dynamique affecte les performances Python. Voici quelques réflexions à ce sujet:

  • La vérification et la conversion de type sont des opérations lourdes. Chaque fois qu'une variable est consultée, lue ou écrite, une vérification de type est effectuée.
  • Un langage avec une telle flexibilité est difficile à optimiser. La raison pour laquelle d'autres langages sont tellement plus rapides que Python est qu'ils compromettent en choisissant entre flexibilité et performances.
  • Le projet Cython combine le typage Python et statique, ce qui, par exemple, comme indiqué dans cet article , conduit à des améliorations des performances 84 fois supérieures à celles de Python standard. Découvrez ce projet si vous avez besoin de vitesse.

Résumé


La raison des mauvaises performances de Python est sa nature dynamique et sa polyvalence. Il peut être utilisé comme un outil pour résoudre une variété de tâches. Pour atteindre les mêmes objectifs, vous pouvez essayer de rechercher des outils plus productifs et mieux optimisés. Peut-être qu'ils pourront trouver, peut-être pas.

Les applications écrites en Python peuvent être optimisées à l'aide des capacités d'exécution de code asynchrone, d'outils de profilage et - en choisissant le bon interprète. Ainsi, pour optimiser la vitesse des applications dont le temps de démarrage n'est pas important et dont les performances peuvent bénéficier de l'utilisation du compilateur JIT, envisagez d'utiliser PyPy. Si vous avez besoin de performances maximales et êtes prêt pour les limitations de la frappe statique, jetez un œil à Cython.

Chers lecteurs! Comment résoudre les problèmes de performances Python médiocres?

Source: https://habr.com/ru/post/fr418823/


All Articles