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_SetTrace
ou PyEval_SetProfile
. Ceci est décrit dans la section documentationProfilage et traçage en Python. Il dit " PyEval_SetTrace
similaire à PyEval_SetProfile
sauf que la fonction de trace reçoit des événements de numéro de ligne".
Le code:
line_profiler
configure son callback en utilisantPyEval_SetTrace
: voirline_profiler.pyx
ligne 157cProfile
configure son callback en utilisantPyEval_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_SetTrace
en 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:
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.py
avec le contenu suivant et comparé le temps d'exécution python -mcProfile test.py
et python test.py
. python. test.py
terminé en environ 0,6 s et en python -mcProfile test.py
environ 1 s. Donc, pour cet exemple particulier, j'ai cProfile
ajouté une surcharge supplémentaire d'environ 60%.
La documentationcProfile
dit:
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
setitimer
pour implémenter un profileur d'échantillonnage, je pense que le stacksampler.py
meilleur 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.py
ne 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.py
est 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
setitimer
et découvrons où dans le code ils appellent setitimer
:
stackprof
(Ruby):stackprof.c
118perftools.rb
(Ruby): , , , , gem (?)stacksampler
(Python):stacksampler.py
51statprof
(Python):statprof.py
239vmprof
(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 setitimer
courte et mérite d'être lue pour toutes les configurations possibles. a souligné un cas d'utilisation intéressant
@mgedmin
sur Twittersetitimer
. Ce problème et ce numéro révèlent plus de détails.
Un inconvénient intéressant des profileurs basés sur
setitimer
- 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
:
pyinstrument
utilisePyEval_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:setitimer
vous permet de profiler uniquement le thread principal en Python)pyflame
profils le code Python en dehors d'un processus à l'aide d'un appel systèmeptrace
. 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-flamegraph
adopte 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:
- Pyflame: Ptracing Profiler d'Uber Engineering pour Python - Introduction à pyflame
- Pyflame Dual Interpreter Mode - Comment il prend en charge à la fois Python 2 et Python 3 en même temps
- Un changement ABI Python inattendu - sur l'ajout de la prise en charge de Python 3.6;
- Décharger des piles Python multi-threadées ;
- Paquets Pyflame ;
ptrace
et les appels système en Python;- Utiliser ptrace pour le plaisir et le profit, ptrace (suite) ;
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
ptrace
s'avérer mieux que process_vm_readv
pour implémenter un profileur Ruby! Il a process_vm_readv
moins 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
vmprof
et stacksampler
- sont similaires (elles ne sont pas -vmprof
prend 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