Les performances ne concernent pas uniquement le CPU: créer vos propres profileurs pour Python

Supposons que votre programme Python soit lent et que vous vous aperceviez que cela n'est que partiellement dû à un manque de ressources processeur . Comment savoir quelles parties du code sont obligées de s'attendre à quelque chose qui ne s'applique pas au CPU?



Après avoir lu le matériel, dont nous publions la traduction aujourd'hui, vous apprendrez à écrire vos propres profileurs pour le code Python. Nous parlons d'outils qui détecteront les endroits du code qui sont inactifs en attendant la libération de certaines ressources. En particulier, nous aborderons les points suivants ici:

  • À quoi peut s'attendre le programme?
  • Profilage de l'utilisation de ressources qui ne sont pas des ressources CPU.
  • Profilage des changements de contexte non intentionnels.

Qu'attend le programme?


Dans les moments où le programme n'est pas occupé par des calculs intensifs à l'aide du processeur, il semble attendre quelque chose. C'est ce qui peut provoquer l'inaction du programme:

  • Ressources réseau. Cela peut inclure l'attente de la fin des recherches DNS, l'attente d'une réponse d'une ressource réseau, l'attente du chargement de certaines données, etc.
  • Disque dur La lecture des données du disque dur peut prendre un certain temps. La même chose peut être dite à propos de l'écriture sur le disque. Parfois, les opérations de lecture ou d'écriture sont effectuées uniquement à l'aide d'un cache situé dans la RAM. Avec cette approche, tout se passe assez rapidement. Mais parfois, lorsqu'un programme interagit directement avec un disque, ces opérations s'avèrent plutôt lentes.
  • Serrures. Un programme peut attendre pour déverrouiller un thread ou un processus.
  • Suspension des travaux. Parfois, un programme peut délibérément suspendre le travail, par exemple, une pause entre les tentatives d'exécution d'une action.

Comment trouver ces endroits de programmes dans lesquels quelque chose se passe qui affecte gravement les performances?

Méthode numéro 1: analyse du temps pendant lequel le programme n'utilise pas le processeur


Le profileur intégré de Python, cProfile , est capable de collecter des données sur de nombreux indicateurs différents liés au fonctionnement des programmes. Pour cette raison, il peut être utilisé pour créer un outil avec lequel vous pouvez analyser le temps pendant lequel le programme n'utilise pas les ressources du processeur.

Le système d'exploitation peut nous dire exactement combien de temps processeur le programme a utilisé.

Imaginez que nous profilons un programme à un seul thread. Les programmes multithread sont plus difficiles à profiler et décrire ce processus n'est pas facile non plus. Si le programme a fonctionné pendant 9 secondes et a utilisé en même temps le processeur pendant 7,5 secondes, cela signifie qu'il a passé 1,5 seconde à attendre.

Créez d'abord une minuterie qui mesurera le délai:

 import os def not_cpu_time():    times = os.times()    return times.elapsed - (times.system + times.user) 

Créez ensuite un profileur qui analyse cette fois:

 import cProfile, pstats def profile_not_cpu_time(f, *args, **kwargs):    prof = cProfile.Profile(not_cpu_time)    prof.runcall(f, *args, **kwargs)    result = pstats.Stats(prof)    result.sort_stats("time")    result.print_stats() 

Après cela, vous pouvez profiler diverses fonctions:

 >>> profile_not_cpu_time( ...   lambda: urlopen("https://pythonspeed.com").read()) ncalls tottime percall filename:lineno(function)    3  0.050  0.017 _ssl._SSLSocket.read    1  0.040  0.040 _socket.getaddrinfo    1  0.020  0.020 _socket.socket.connect    1  0.010  0.010 _ssl._SSLSocket.do_handshake  342  0.010  0.000 find.str  192  0.010  0.000 append.list 

Les résultats nous permettent de conclure que la plupart du temps a été consacré à la lecture des données du socket, mais il a fallu un certain temps pour effectuer des recherches DNS ( getaddrinfo ), ainsi que pour effectuer des getaddrinfo TCP ( connect ) et TLS / SSL.

Puisque nous nous sommes assurés d'enquêter sur les périodes de fonctionnement du programme pendant lesquelles il n'utilise pas les ressources du processeur, nous savons que tout cela n'est que du temps d'attente pur, c'est-à-dire le moment où le programme n'est pas occupé par des calculs.

Pourquoi le temps est-il enregistré pour str.find et list.append ? Lors de l'exécution de telles opérations, le programme n'a rien à attendre, donc l'explication semble plausible, selon laquelle nous avons affaire à une situation où l'ensemble du processus n'a pas été effectué. Peut-être - en attente de la fin d'un autre processus, ou en attendant la fin du chargement des données en mémoire à partir du fichier d'échange. Cela indique qu'un certain temps a été consacré à l'exécution de ces opérations, ce qui ne fait pas partie du temps du processeur.

De plus, je tiens à noter que j'ai vu des rapports qui contiennent de petits fragments négatifs de temps. Cela implique une certaine différence entre le temps écoulé et le temps processeur, mais je ne m'attends pas à ce que cela ait un impact significatif sur l'analyse des programmes plus complexes.

Méthode numéro 2: analyse du nombre de changements de contexte intentionnels


Le problème avec la mesure du temps passé par le programme à attendre quelque chose est que, lors de l'exécution de différentes sessions de mesure pour le même programme, il peut varier en raison de quelque chose qui est en dehors de la portée du programme. Parfois, les requêtes DNS peuvent être plus lentes que d'habitude. Parfois, plus lentement que d'habitude, certaines données peuvent se charger. Par conséquent, il serait utile d'utiliser des indicateurs plus prévisibles qui ne sont pas liés à la vitesse de ce qui entoure le programme.

Une façon de procéder consiste à calculer le nombre d'opérations qui doivent attendre ont terminé le processus. Autrement dit, nous parlons de calculer le nombre de périodes d'attente, et non le temps passé à attendre quelque chose.

Un processus peut cesser d'utiliser les ressources du processeur pour deux raisons:

  1. Chaque fois qu'un processus effectue une opération qui ne se termine pas instantanément, par exemple, il lit des données à partir d'un socket, fait une pause, etc., cela équivaut à ce qu'il dit au système d'exploitation: "Réveillez-moi lorsque je pourrai continuer à travailler." C'est ce que l'on appelle le «changement de contexte délibéré»: le processeur peut basculer vers un autre processus jusqu'à ce que les données apparaissent sur le socket, ou jusqu'à ce que notre processus quitte le mode veille, ainsi que dans d'autres cas similaires.
  2. La «commutation de contexte involontaire» est une situation dans laquelle le système d'exploitation arrête temporairement un processus, permettant à un autre processus de tirer parti des ressources du processeur.

Nous profilerons les changements de contexte intentionnels.

psutil un profileur qui compte les changements de contexte intentionnels à l'aide de la bibliothèque psutil :

 import psutil _current_process = psutil.Process() def profile_voluntary_switches(f, *args, **kwargs):    prof = cProfile.Profile(        lambda: _current_process.num_ctx_switches().voluntary)    prof.runcall(f, *args, **kwargs)    result = pstats.Stats(prof)    result.sort_stats("time")    result.print_stats() 

Maintenant, profilons à nouveau le code qui fonctionne avec le réseau:

 >>> profile_voluntary_switches( ...   lambda: urlopen("https://pythonspeed.com").read()) ncalls tottime percall filename:lineno(function)     3  7.000  2.333 _ssl._SSLSocket.read     1  2.000  2.000 _ssl._SSLSocket.do_handshake     1  2.000  2.000 _socket.getaddrinfo     1  1.000  1.000 _ssl._SSLContext.set_default_verify_path     1  1.000  1.000 _socket.socket.connect 

Maintenant, au lieu des données de temps d'attente, nous pouvons voir des informations sur le nombre de changements de contexte intentionnels qui se sont produits.

Notez que vous pouvez parfois voir des changements de contexte intentionnels dans des endroits inattendus. Je crois que cela se produit lorsque les données du fichier d'échange sont chargées en raison d'erreurs de page mémoire.

Résumé


L'utilisation de la technique de profilage de code décrite ici crée une certaine charge supplémentaire sur le système, ce qui ralentit considérablement le programme. Dans la plupart des cas, cependant, cela ne devrait pas conduire à une distorsion significative des résultats en raison du fait que nous n'analysons pas l'utilisation des ressources du processeur.

En général, on peut noter que tout indicateur mesurable lié aux travaux du programme se prête au profilage. Par exemple, les éléments suivants:

  • Nombre de lectures ( psutil.Process().read_count ) et d'écritures ( psutil.Process().write_count ).
  • Sous Linux, le nombre total d'octets lus et écrits (psutil. Process().read_chars ).
  • Indicateurs d'allocation de mémoire (effectuer une telle analyse nécessitera un certain effort; cela peut être fait en utilisant jemalloc ).

Les détails sur les deux premiers éléments de cette liste se trouvent dans la documentation psutil .

Chers lecteurs! Comment profilez-vous vos applications Python?

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


All Articles