Les ennemis de Python disent toujours que l'une des raisons pour lesquelles ils ne veulent pas utiliser ce langage est que Python est lent. Mais le fait qu'un certain programme, quel que soit le langage de programmation utilisé, puisse être considéré comme rapide ou lent, dépend beaucoup du développeur qui l'a écrit, de ses connaissances et de la capacité à créer du code optimisé et performant.

L'auteur de l'article, que nous publions aujourd'hui, propose de prouver que ceux qui appellent Python lent ont tort. Il veut parler de la façon d'améliorer les performances des programmes Python et de les rendre très rapides.
Mesure du temps et profilage
Avant de commencer à optimiser un code, vous devez d'abord savoir quelles parties de celui-ci ralentissent l'ensemble du programme. Parfois, un goulot d'étranglement peut être évident, mais si le programmeur ne sait pas où il se trouve, il peut profiter de certaines opportunités pour l'identifier.
Vous trouverez ci-dessous le code du programme, que j'utiliserai à des fins de démonstration. Il est extrait de la documentation Python. Ce code élève
e
à la puissance de
x
:
La façon la plus simple de «profiler» le code
Pour commencer, envisagez la façon la plus simple de profiler votre code. Pour ainsi dire, «profilage pour les paresseux». Elle consiste à utiliser la commande de
time
Unix:
~ $ time python3.8 slow_program.py real 0m11,058s user 0m11,050s sys 0m0,008s
Un tel profilage pourrait bien donner au programmeur des informations utiles - dans le cas où il aurait besoin de mesurer le temps d'exécution de l'ensemble du programme. Mais ce n'est généralement pas suffisant.
La méthode de profilage la plus précise
À l'autre extrémité du spectre des méthodes de profilage de code se trouve l'outil
cProfile
, qui donne, certes, trop d'informations au programmeur:
~ $ python3.8 -m cProfile -s time slow_program.py 1297 function calls (1272 primitive calls) in 11.081 seconds Ordered by: internal time ncalls tottime percall cumtime percall filename:lineno(function) 3 11.079 3.693 11.079 3.693 slow_program.py:4(exp) 1 0.000 0.000 0.002 0.002 {built-in method _imp.create_dynamic} 4/1 0.000 0.000 11.081 11.081 {built-in method builtins.exec} 6 0.000 0.000 0.000 0.000 {built-in method __new__ of type object at 0x9d12c0} 6 0.000 0.000 0.000 0.000 abc.py:132(__new__) 23 0.000 0.000 0.000 0.000 _weakrefset.py:36(__init__) 245 0.000 0.000 0.000 0.000 {built-in method builtins.getattr} 2 0.000 0.000 0.000 0.000 {built-in method marshal.loads} 10 0.000 0.000 0.000 0.000 <frozen importlib._bootstrap_external>:1233(find_spec) 8/4 0.000 0.000 0.000 0.000 abc.py:196(__subclasscheck__) 15 0.000 0.000 0.000 0.000 {built-in method posix.stat} 6 0.000 0.000 0.000 0.000 {built-in method builtins.__build_class__} 1 0.000 0.000 0.000 0.000 __init__.py:357(namedtuple) 48 0.000 0.000 0.000 0.000 <frozen importlib._bootstrap_external>:57(_path_join) 48 0.000 0.000 0.000 0.000 <frozen importlib._bootstrap_external>:59(<listcomp>) 1 0.000 0.000 11.081 11.081 slow_program.py:1(<module>)
Ici, nous
cProfile
le script étudié à l'aide du module
cProfile
et utilisons l'argument
time
. En conséquence, les lignes de sortie sont triées par heure interne (
cumtime
). Cela nous donne beaucoup d'informations. En fait, ce qui est indiqué ci-dessus ne représente qu'environ 10% de la sortie de
cProfile
.
Après avoir analysé ces données, nous pouvons voir que la fonction
exp
est la raison du lent fonctionnement du programme (c'est une surprise!). Après cela, nous pouvons faire du profilage de code en utilisant des outils plus précis.
L'étude des indicateurs de performance temporaires d'une fonction spécifique
Nous connaissons maintenant la place du programme sur laquelle nous devons diriger notre attention. Par conséquent, nous pouvons décider d'étudier la fonction lente sans profiler d'autres codes de programme. Pour ce faire, vous pouvez utiliser un simple décorateur:
def timeit_wrapper(func): @wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter()
Ce décorateur peut être appliqué à la fonction à explorer:
@timeit_wrapper def exp(x): ... print('{0:<10} {1:<8} {2:^8}'.format('module', 'function', 'time')) exp(Decimal(150)) exp(Decimal(400)) exp(Decimal(3000))
Maintenant, après le démarrage du programme, nous recevrons les informations suivantes:
~ $ python3.8 slow_program.py module function time __main__ .exp : 0.003267502994276583 __main__ .exp : 0.038535295985639095 __main__ .exp : 11.728486061969306
Ici, il convient de prêter attention à l'heure exacte que nous prévoyons de mesurer. Le package correspondant nous fournit des indicateurs tels que
time.perf_counter
et
time.process_time
. La différence entre eux est que
perf_counter
renvoie une valeur absolue, qui inclut le temps pendant lequel le processus du programme Python ne s'exécute pas. Cela signifie que cet indicateur peut être affecté par la charge sur l'ordinateur créée par d'autres programmes. La
process_time
renvoie uniquement le temps utilisateur. Il n'inclut pas l'heure système. Cela nous donne uniquement des informations sur le temps d'exécution de notre processus.
Accélération du code
Et maintenant pour la partie amusante. Travaillons à accélérer le programme. Je ne vais (pour la plupart) pas montrer ici toutes sortes de hacks, astuces et morceaux de code mystérieux qui résolvent comme par magie les problèmes de performances. Je veux essentiellement parler d'idées et de stratégies communes qui, si elles sont utilisées, peuvent avoir un impact important sur les performances. Dans certains cas, nous parlons d'une augmentation de 30% de la vitesse d'exécution du code.
▍Utilisez les types de données intégrés
L'utilisation de types de données intégrés est une approche très évidente pour accélérer le code. Les types de données intégrés sont extrêmement rapides, surtout si vous les comparez avec des types personnalisés comme des arbres ou des listes liées. Le point ici est principalement que les mécanismes intégrés du langage sont implémentés en utilisant C. Si vous décrivez quelque chose en utilisant Python, vous ne pouvez pas atteindre le même niveau de performances.
▍Appliquer la mise en cache (mémorisation) avec lru_cache
La mise en cache est une approche populaire pour améliorer les performances du code. J'ai déjà
écrit à son sujet, mais je pense qu'il vaut la peine d'en parler ici:
import functools import time
La fonction ci-dessus simule des calculs complexes en utilisant
time.sleep
. Lorsqu'il est appelé pour la première fois avec le paramètre
1
, il attend 2 secondes et ne renvoie le résultat qu'après cela. Lorsqu'elle est à nouveau appelée avec le même paramètre, il s'avère que le résultat de son travail est déjà mis en cache. Le corps de la fonction dans cette situation n'est pas exécuté et le résultat est renvoyé immédiatement. Vous trouverez ici des exemples de mise en cache plus proches de la réalité.
▍Utiliser des variables locales
En appliquant des variables locales, nous prenons en compte la vitesse de recherche d'une variable dans chaque portée. Je parle spécifiquement de "tous les domaines de visibilité", car ici je n'ai pas seulement en tête une comparaison de la vitesse de travail avec les variables locales et globales. En fait, la différence de travail avec les variables est même observée, par exemple, entre les variables locales dans une fonction (la vitesse la plus élevée), les attributs au niveau de la classe (par exemple,
self.name
, c'est déjà plus lent) et les entités importées globales comme
time.time
(le plus lent de ces trois mécanismes).
Vous pouvez améliorer les performances en utilisant les approches suivantes pour attribuer des valeurs qui peuvent sembler complètement inutiles et inutiles à une personne non informée:
▍ Envelopper le code dans la fonction
Ce conseil peut sembler contraire au bon sens, car lorsqu'une fonction est appelée, certaines données sont poussées sur la pile et le système subit une charge supplémentaire qui traite l'opération de retour de la fonction. Cependant, cette recommandation est liée à la précédente. Si vous venez de mettre tout votre code dans un fichier sans l'écrire en tant que fonction, il s'exécutera beaucoup plus lentement en raison de l'utilisation de variables globales. Cela signifie que le code peut être accéléré en l'enveloppant simplement dans la fonction
main()
et en l'appelant une fois:
def main(): ...
▍Ne pas accéder aux attributs
Un autre mécanisme qui peut ralentir un programme est l'opérateur point (
.
), Qui est utilisé pour accéder aux attributs des objets. Cette instruction appelle une
__getattribute__
dictionnaire à l'aide de
__getattribute__
, ce qui
__getattribute__
un stress supplémentaire sur le système. Comment limiter l'impact sur les performances de cette fonctionnalité Python?
▍Méfiez-vous des chaînes
Les opérations sur chaîne peuvent ralentir considérablement un programme si elles sont exécutées en boucle. En particulier, nous parlons de formatage de chaînes à l'aide de
%s
et
.format()
. Est-il possible de les remplacer par quelque chose? Si vous regardez un
tweet récent
de Raymond Hettinger, vous pouvez voir que le seul mécanisme qui doit être utilisé dans de telles situations est les lignes f. Il s'agit de la méthode de formatage de chaîne la plus lisible, concise et la plus rapide. Voici, conformément à ce tweet, une liste de méthodes qui peuvent être utilisées pour travailler avec des chaînes - de la plus rapide à la plus lente:
f'{s} {t}'
▍Sachez que les générateurs peuvent également fonctionner rapidement
Les générateurs ne sont pas ces mécanismes qui, de par leur nature, sont rapides. Le fait est qu'ils ont été créés pour effectuer des calculs «paresseux», ce qui fait gagner non pas du temps, mais de la mémoire. Cependant, économiser de la mémoire peut accélérer l'exécution des programmes. Comment est-ce possible? Le fait est que lors du traitement d'un grand ensemble de données sans utiliser de générateurs (itérateurs), les données peuvent entraîner un débordement du cache L1 du processeur, ce qui ralentira considérablement le processus de recherche de valeurs en mémoire.
En ce qui concerne les performances, il est très important de veiller à ce que le processeur puisse accéder rapidement aux données qu'il traite, afin qu'elles soient aussi proches que possible de celles-ci. Et cela signifie que ces données doivent être placées dans le cache du processeur. Cette question est abordée dans
cette présentation par Raymond Hettinger.
Résumé
La première règle d'optimisation est que l'optimisation n'est pas nécessaire. Mais si vous ne pouvez pas vous passer de l'optimisation, j'espère que les conseils que j'ai partagés vous y aideront.
Chers lecteurs! Comment abordez-vous l'optimisation des performances de votre code Python?
