Comment les profileurs fonctionnent-ils dans Ruby et Python?

La traduction de l'article a été préparée en prévision du début du cours avancé "Développeur Python" .



L'article original peut être lu ici .










salut! En guise d'apéritif au profileur Ruby, je voulais parler du fonctionnement des profileurs Ruby et Python existants. Cela permettra également de répondre à la question que beaucoup de gens me posent: "Comment écrire un profileur?"



Dans cet article, nous nous concentrerons sur les profileurs de processeur (et non, disons, les profileurs de mémoire / tas). Je vais couvrir quelques approches de base pour écrire un profileur, fournir des exemples de code et montrer de nombreux exemples de profileurs populaires en Ruby et Python, et également vous montrer comment ils fonctionnent sous le capot.



Probablement, il peut y avoir des erreurs dans l'article (en préparation pour l'écrire, j'ai partiellement parcouru le code de 14 bibliothèques pour le profilage, et je ne connaissais pas beaucoup d'entre elles jusqu'à présent), alors faites-le moi savoir si vous les trouvez ...



2 types de profileurs



Il existe deux principaux types de profileurs de processeur: les profileurs d' échantillonnage et de traçage .



Les profileurs de traçage enregistrent chaque appel de fonction dans votre programme, fournissant finalement un rapport. Les profileurs d'échantillonnage adoptent une approche statistique, ils écrivent la pile toutes les quelques millisecondes, générant un rapport basé sur ces données.



La principale raison d'utiliser un profileur d'échantillonnage au lieu d'un profileur de traçage est qu'il est léger. Vous prenez 20 ou 200 photos par seconde - cela ne prend pas beaucoup de temps. De tels profileurs seront très efficaces si vous avez un sérieux problème de performances (80% du temps est passé à appeler une fonction lente), puisque 200 snapshots par seconde suffiront à identifier la fonction problématique!



Profileurs



Ensuite, je donnerai un résumé général des profileurs discutés dans cet article (à partir d'ici ). J'expliquerai les termes utilisés dans cet article ( setitimer , rb_add_event_hook , ptrace ) un peu plus tard. Fait intéressant, tous les profileurs sont implémentés à l'aide d'un petit ensemble de fonctionnalités de base.



Profileurs Python







"Gdb hacks" n'est pas vraiment un profileur Python - il renvoie à un site Web qui explique comment implémenter un profileur de hacker comme wrapper de script shell autour de gdb . Il s'agit spécifiquement de Python, car les nouvelles versions de gbd déploieront en fait la pile Python pour vous. Quelque chose comme pyflame pour les pauvres.



Profileurs de rubis







Presque tous ces profileurs vivent dans votre processus



Avant d'entrer dans les détails de ces profileurs, il y a une chose très importante: tous ces profileurs, à l'exception de pyflame , s'exécutent dans votre processus Python / Ruby. Si vous êtes dans un programme Python / Ruby, vous avez généralement un accès facile à la pile. Par exemple, voici un programme Python simple qui imprime le contenu de la pile de chaque thread en cours d'exécution:



import sys
import traceback

def bar():
    foo()

def foo():
    for _, frame in sys._current_frames().items():
        for line in traceback.extract_stack(frame):
            print line

bar()


Voici la sortie de la console. Vous pouvez voir qu'il a des noms de fonction de la pile, des numéros de ligne, des noms de fichiers - tout ce dont vous pourriez avoir besoin si vous effectuez un profilage.



('test2.py', 12, '<module>', 'bar()')
('test2.py', 5, 'bar', 'foo()')
('test2.py', 9, 'foo', 'for line in traceback.extract_stack(frame):')


C'est encore plus simple dans Ruby: vous pouvez utiliser met caller pour obtenir la pile.



La plupart de ces profileurs sont des extensions de performances de C, donc ils diffèrent légèrement, mais ces extensions pour les programmes Ruby / Python ont également un accès facile à la pile d'appels.



Fonctionnement des profileurs de traçage



J'ai répertorié tous les profils de traçage Ruby et Python dans les tableaux ci-dessus: rblineprof, ruby-prof, line_profiler et cProfile . Ils fonctionnent tous de la même manière. Ils enregistrent chaque appel de fonction et sont des extensions C pour réduire la surcharge.



Comment travaillent-ils? Dans Ruby et Python, vous pouvez spécifier un rappel qui est déclenché lorsque divers événements d'interprétation se produisent (par exemple, «un appel de fonction» ou «une ligne d'exécution de code»). Lorsque le rappel est appelé, il écrit la pile pour une analyse ultérieure.



Il est utile de voir exactement où se trouvent ces rappels dans le code, je vais donc créer un lien vers les lignes de code pertinentes sur github.



En Python, vous pouvez personnaliser le rappel avec PyEval_SetTraceou PyEval_SetProfile. Ceci est décrit dans la section documentationProfilage et traçage en Python. Il dit " PyEval_SetTracesimilaire à PyEval_SetProfilesauf que la fonction de trace reçoit des événements de numéro de ligne".



Le code:



  • line_profilerconfigure son callback en utilisant PyEval_SetTrace: voir line_profiler.pyxligne 157
  • cProfileconfigure son callback en utilisant PyEval_SetProfile: voir _lsprof.c ligne 693 (cProfile est implémenté en utilisant lsprof )


Dans Ruby, vous pouvez personnaliser votre rappel avec rb_add_event_hook. Je n'ai trouvé aucune documentation à ce sujet, mais voici à quoi ça ressemble.



rb_add_event_hook(prof_event_hook,
      RUBY_EVENT_CALL | RUBY_EVENT_RETURN |
      RUBY_EVENT_C_CALL | RUBY_EVENT_C_RETURN |
      RUBY_EVENT_LINE, self);


Signature prof_event_hook:



static void
prof_event_hook(rb_event_flag_t event, VALUE data, VALUE self, ID mid, VALUE klass)




Quelque chose comme PyEval_SetTraceen Python, mais sous une forme plus flexible - vous pouvez choisir les événements pour lesquels vous souhaitez être notifié (par exemple, "uniquement les appels de fonction").



Le code:



  • ruby-prof rb_add_event_hook : ruby-prof.c 329
  • rblineprof rb_add_event_hook : rblineprof.c 649


tracing-



Le principal inconvénient des profileurs de traçage mis en œuvre de cette manière est qu'ils ajoutent une quantité fixe de code pour chaque appel de fonction / ligne qui est exécuté. Cela peut vous faire prendre de mauvaises décisions! Par exemple, si vous avez deux implémentations de quelque chose - l'une avec beaucoup d'appels de fonction et l'autre sans, qui prennent généralement le même temps, alors la première avec beaucoup d'appels de fonction semblera plus lente lors du profilage.



Pour illustrer, j'ai créé un petit fichier nommé test.pyavec le contenu suivant et comparé le temps d'exécution python -mcProfile test.pyet python test.py. python. test.pyterminé en environ 0,6 s et en python -mcProfile test.pyenviron 1 s. Donc, pour cet exemple particulier, j'ai cProfileajouté une surcharge supplémentaire d'environ 60%.

La documentation cProfiledit:

la nature interprétée de Python ajoute tellement de temps d'exécution que le profilage déterministe a tendance à ajouter un peu de surcharge de traitement dans les applications normales.


Cela semble être une déclaration assez raisonnable - l'exemple précédent (qui fait 3,5 millions d'appels de fonction et rien d'autre) n'est évidemment pas un programme Python ordinaire, et presque tous les autres programmes auront moins de surcharge.

Je n'ai pas vérifié la surcharge ruby-prof(profileur de traçage Ruby), mais son README dit ce qui suit:

La plupart des programmes fonctionneront environ deux fois moins lentement, tandis que les programmes hautement récursifs (comme le test de la série Fibonacci) fonctionneront trois fois plus lentement .


Fonctionnement général des profileurs d'échantillonnage: setitimer



Il est temps de parler du deuxième type de profileur: les profileurs d'échantillonnage!

La plupart des profileurs d'échantillonnage dans Ruby et Python sont implémentés à l'aide d'un appel système setitimer. Ce que c'est?



Supposons que vous souhaitiez prendre un instantané de la pile de programmes 50 fois par seconde. Cela peut être fait comme suit:



  • Demandez au noyau Linux de vous envoyer un signal toutes les 20 millisecondes (en utilisant un appel système setitimer);
  • Enregistrer un gestionnaire de signal pour un instantané de pile lorsqu'un signal est reçu;
  • Une fois le profilage terminé, demandez à Linux d'arrêter de vous signaler et de fournir le résultat!


Si vous voulez voir un cas d'utilisation pratique setitimerpour implémenter un profileur d'échantillonnage, je pense que le stacksampler.pymeilleur exemple est un profileur utile et fonctionnel, et il fait environ 100 lignes en Python. Et c'est très cool!



La raison pour laquelle cela stacksampler.pyne prend que 100 lignes en Python est que lorsque vous enregistrez une fonction Python en tant que gestionnaire de signal, la fonction est transmise à la pile actuelle de votre programme. Par conséquent, il stacksampler.pyest très facile d'enregistrer un gestionnaire de signaux :



def _sample(self, signum, frame):
   stack = []
   while frame is not None:
       stack.append(self._format_frame(frame))
       frame = frame.f_back

   stack = ';'.join(reversed(stack))
   self._stack_counts[stack] += 1


Il fait simplement sortir une pile d'une image et incrémente le nombre de fois qu'une pile particulière a été vue. Si simple! Trop cool!



Jetons un coup d'œil à tous nos autres profileurs qu'ils utilisent setitimeret découvrons où dans le code ils appellent setitimer:



  • stackprof (Ruby): stackprof.c 118
  • perftools.rb (Ruby): , , , , gem (?)
  • stacksampler (Python): stacksampler.py 51
  • statprof (Python): statprof.py 239
  • vmprof (Python): vmprof_unix.c 294


La chose importante à propos de setitimer- vous devez décider comment compter le temps. Voulez-vous 20 ms en temps réel? 20 ms de temps CPU utilisateur? 20 ms utilisateur + temps du processeur système? Si vous regardez attentivement les liens ci-dessus, vous remarquerez que ces profileurs utilisent en fait des choses différentes setitimer- parfois le comportement est personnalisable et parfois non. La page de manuel est setitimercourte et mérite d'être lue pour toutes les configurations possibles. a souligné un cas d'utilisation intéressant



@mgedminsur Twittersetitimer . Ce problème et ce numéro révèlent plus de détails.



Un inconvénient intéressant des profileurs basés sursetitimer- quelles minuteries déclenchent des signaux! Les signaux interrompent parfois les appels système! Les appels système prennent parfois quelques millisecondes! Si vous prenez des instantanés trop souvent, vous pouvez faire en sorte que le programme exécute des appels système indéfiniment!



Profileurs d'échantillonnage qui n'utilisent pas setitimer



Il existe plusieurs profileurs d'échantillonnage qui n'utilisent pas setitimer:



  • pyinstrumentutilise PyEval_SetProfile(donc c'est une sorte de profileur de traçage), mais il ne collecte pas toujours les instantanés de la pile lorsque le rappel de trace est appelé. Voici le code qui sélectionne la synchronisation de l'instantané de trace de pile . En savoir plus sur cette solution dans ce blog . (en gros: setitimervous permet de profiler uniquement le thread principal en Python)
  • pyflameprofils le code Python en dehors d'un processus à l'aide d'un appel système ptrace. Il utilise une boucle où il prend des photos, dort pendant un certain temps et refait la même chose. Voici un appel à attendre.
  • python-flamegraphadopte une approche similaire où il démarre un nouveau thread dans votre processus Python et obtient des traces de pile, se met en veille et réessaye à nouveau. Voici un appel à attendre .


Tous les 3 de ces profileurs prennent des instantanés en temps réel.



Articles de blog Pyflame



J'ai passé presque tout mon temps dans cet article sur des profileurs autres que pyflame, mais cela m'intéresse le plus car il profile votre programme Python à partir d'un processus séparé, c'est pourquoi je veux que mon profileur Ruby fonctionne de la même manière.



Bien sûr, tout est un peu plus compliqué que ce que j'ai décrit. Je n'entrerai pas dans les détails, mais Evan Klitzke a écrit de nombreux bons articles à ce sujet sur son blog:





Plus d'informations peuvent être trouvées sur eklitzke.org . Ce sont toutes des choses très intéressantes que je vais lire de plus près - cela pourrait ptraces'avérer mieux que process_vm_readvpour implémenter un profileur Ruby! Il a process_vm_readvmoins de frais généraux car il n'arrête pas le processus, mais il peut également vous donner un instantané incorrect car il n'arrête pas le processus :). Dans mes expériences, obtenir des images contradictoires n'était pas un gros problème, mais je pense qu'ici je vais mener une série d'expériences.



C'est tout pour aujourd'hui!



Il y a de nombreuses subtilités importantes que je n'ai pas abordées dans cet article - par exemple, j'ai essentiellement dit cela vmprofet stacksampler- sont similaires (elles ne sont pas -vmprofprend en charge le profilage de chaînes et le profilage des fonctions Python écrites en C, ce qui, je crois, rend le profileur plus complexe). Mais ils ont certains des mêmes principes de base, et je pense donc que l'examen d'aujourd'hui sera un bon point de départ.






TDD avec et sans pytest. Webinaire gratuit






Lire la suite:






All Articles