Introduction
perf
(également appelé perf_events
) est un ensemble d’outils pour analyser
la performance des systèmes Linux. Ces outils s’appuient sur une infrastructure
présente dans le noyau Linux depuis sa version 2.6.31 (2009), avec laquelle ils
entretiennent une relation très étroite, les outils perf
étant maintenus au
sein du code source du noyau Linux. Une analyse à grain fin de l’activité CPU
est également possible via des compteurs de performance (Performance
Monitoring Unit ou PMU), définis par chaque microarchitecture CPU.
En seulement une dizaine d’années d’existence, perf
est devenu une référence
dans plusieurs domaines, incluant notamment l’étude à grain fin de l’activité
CPU et l’instrumentation dynamique de code noyau et applicatif. Dans ce TP, nous
allons explorer les différentes possibilités offertes par perf
aujourd’hui
(2022), en l’utilisant pour analyser l’exécution de programmes simples mais
néanmoins représentatifs des différentes formes d’activité logicielle qu’il sait
décortiquer.
Le TP est pensé pour être suivi dans l’ordre, chaque section pouvant ainsi se référer aux informations présentées dans la section précédente. Il se décompose en trois grandes parties :
- Dans une première partie, nous étudierons les fonctionnalités élémentaires que
tout utilisateur de
perf
devrait connaître, du simple comptage d’événements (perf stat
) au profilage de code (perf report
) en passant par le suivi d’activité noyau (perf trace
). - Dans une seconde partie, nous aborderons des fonctionnalités plus avancées,
allant du profilage en temps réel du système entier (
perf top
) à l’analyse détaillée des accès mémoire (perf mem
,perf c2c
) en passant par l’instrumentation de code arbitraire (perf probe
). - Enfin, des annexes abordent plusieurs thématiques qui ne sont pas couvertes en TP pour diverses raisons (manque de temps, intérêt plus faible, nécessitent des privilèges trop importants…), mais sont utiles à connaître dans la pratique quotidienne de l’outil.
Le TP est pensé pour être suivi sur srv-calcul-ambulant
, nous supposerons donc
que vous avez déjà un compte sur cette plate-forme et savez vous en servir. En
cas de besoin, une copie de la documentation du serveur est disponible en local
à l’URL http://srv-calcul-ambulant/ 🇬🇧, et cette documentation est également
publiée sur Internet 🇬🇧.
Si vous souhaitez adapter le TP à une autre plate-forme, son code source est
disponible sur https://gitlab.in2p3.fr/grasland/tp-perf sous licence
CC-BY-SA 4.0. Le rendu
Markdown est assuré par mdBook et les
petits programmes utilisés pour les TPs sont stockés dans le répertoire code/
.
perf stat
perf
est construit autour de la notion de gestion d’événements système.
Un événement au sens de perf
peut être toutes sortes de choses, allant du
passage d’un cycle processeur à un appel système en passant par des défauts de
cache CPU et des écritures disque. L’idée centrale de perf
est d’abstraire
autant que possible la différence entre ces diverses sortes d’événements, pour
pouvoir effectuer de nombreuses activités d’analyse de performances avec un
jeu d’outils d’analyse relativement petit.
La chose la plus simple que perf
puisse faire avec des événements, c’est de
les compter via la commande perf stat
, et c’est donc par cette activité que
nous allons démarrer ce TP.
Premier contact
Au cours de ce TP, vous serez amenés à compiler et exécuter des codes d’exemple. Commençons par télécharger et extraire ceux-ci dans votre répertoire personnel :
cd ~
curl -L http://srv-calcul-ambulant/docs/tp-perf/tp-perf.tar.gz | tar -xz
cd tp-perf
N’oubliez pas de compiler avec srun
pour ne pas surcharger les coeurs CPU
interactifs :
srun make -j$(nproc)
Pour introduire perf stat
, nous allons commencer par un code d’exemple
extrêmement simple inspiré du benchmark STREAM, appelé scale
. Ce code part
d’un tableau de nombres flottants, en multiplie chaque élément par une
constante, et stocke le résultat dans un autre tableau. Il prend deux
paramètres, le premier est la taille des tableaux, et le second est le nombre de
fois que le calcul doit être effectué.
Par exemple, pour multiplier deux mille flottants dix millions de fois, nous ferions…
srun --pty ./scale.bin 2048 10000000
Partant de là, pour instrumenter l’exécution de cet exemple avec perf stat, il
suffit d’injecter une commande perf stat
avant le nom de l’exécutable,
comme ceci :
srun --pty perf stat ./scale.bin 2048 10000000
Et nous obtenons ce genre de résultats :
On voit que par défaut, perf stat
nous donne les mêmes informations que la
commande time
(wall-clock time, user time, system time), mais qu’en complément
il nous donne aussi…
- Des informations sur l’activité système analogues à celles fournies par GNU time :
- Changements de contexte
- Migrations d’une tâche entre coeurs CPU logiques
- Défauts de page
- Des informations microarchitecturales sur l’activité CPU :
- Nombre de cycles CPU écoulés
- Nombre d’instructions exécutées
- Nombre de branchements (if, boucles…) effectués
- Nombre de branchements mal prédits
Par ailleurs, sur la colonne de droite, vous remarquerez que perf stat déduit automatiquement pour vous de ces mesures brutes certaines quantités plus utiles pour une analyse de performances :
- Les nombres d’événements système sont ramenés à une fréquence par seconde
- Le temps CPU écoulé est traduit en nombre de CPUs utilisés (en divisant par le temps réel)
- Les cycles sont traduits en fréquence CPU moyenne (en divisant par le temps CPU)
- Les instructions sont traduits en instructions par cycle
- Le nombre de branchements mal prédits est ramené à un taux de branchements mal prédits
Il est utile de connaître la fréquence CPU à laquelle on travaille quand on
évalue la performance de son code en termes de limites absolues du système à
partir d’un temps d’exécution. Par exemple, ici, notre processeur est capable
d’effectuer 2 multiplications flottantes vectorisées par cycle avec une largeur
de vecteur de 512 bits, donc à une fréquence d’horloge de 3,905 GHz et en simple
précision (32 bits), nous pourrions nous attendre à calculer
opérations flottantes par seconde. Mais nous n’en calculons que
. Par la
suite, nous utiliserons perf
pour comprendre pourquoi nous calculons ~8.5x
plus lentement que prévu.
Le nombre d’instructions exécutées par cycle est une statistique plus difficile à interpréter, dans la mesure où la valeur optimale dépend du calcul qu’on est en train d’effectuer et du modèle de CPU qu’on utilise. Mais on peut garder en tête quelques ordres de grandeur :
- Même les unités de calcul les plus sollicitées (chargement depuis la mémoire, ALUs vectorielles…) n’existent qu’en deux exemplaires sur la plupart des CPUs. Donc un code qui atteint >= 2 instructions par cycle est potentiellement limité par les unités de calcul du CPU.
- Les CPU modernes sont conçus pour effectuer du travail en continu, donc un nombre d’instructions par cycle < 1 (qui signifie que le CPU n’exécute aucune nouvelle instruction durant certains cycles) est suspect et mérite un examen plus approfondi.
Exercice : Essayez de relancer le calcul à nombre de calculs constant, mais
avec un tableau de plus en plus grand (par exemple, multipliez le premier
paramètre par 10 et divisez le second par 10, plusieurs fois de suite). Observez
les changements dans la sortie de perf stat
alors que la performance de calcul
devient limitée par la bande passante du cache L2, puis celle du cache L3, puis
celle de la RAM.
Notez que pour vraiment saturer le cache L3 et la bande passante RAM, vous aurez
besoin d’utiliser tous les coeurs CPU. Vous pouvez le faire en ajoutant le
paramètre --exclusive
à srun
, mais comme cela empêche les autres
participants du TP de lancer des jobs pendant la durée du vôtre, je vous
demanderai d’être particulièrement attentif à ce que vos jobs exclusifs soient
de courte durée (quelques secondes).
Pendant qu’on parle de multi-threading, essayez d’ajouter l’option
--hint=nomultithread
à srun
pour désactiver l’hyperthreading. Slurm ne vous
allouera alors qu’un seul hyperthread au lieu des deux du coeur CPU que vous
obtenez quand vous lancer srun
sans argument supplémentaire. Comparez les
sorties de perf stat
dans ces deux configurations, que constatez-vous ?
Statistiques détaillées
Avec l’option --detailed
, qui s’abbrévie en -d
, on peut demander à
perf stat
d’afficher des statistiques plus détaillées incluant l’activité des
différents niveaux de cache CPU.
srun --pty \
perf stat -d \
./scale.bin 2048 10000000
Vous constaterez toutefois qu’en complément de ces nouvelles statistiques (où LLC signifie Last Level Cache, et désigne donc le cache L3 sur les processeurs Intel actuels qui ont trois niveau de cache), des pourcentages entre parenthèses sont apparus à droite de l’affichage.
Ces pourcentages sont la manière de perf
de nous avertir que nous sommes un
peu gourmands et demandons davantage de données que le CPU n’est capable d’en
surveiller à un instant T. Dans ce cas, perf
doit simuler un CPU pouvant
effectuer toutes les mesures demandées par multiplexage temporel : il alterne
entre les différents types de mesures demandés et extrapole ces résultats de
mesure partiels en supposant que l’activité du CPU est constante au fil du temps.
Le pourcentage indique pendant quel pourcentage du temps le compteur de performance CPU était réellement actif. Plus il est petit, plus la mesure est extrapolée, donc moins elle est fiable.
Si l’on veut éviter toute extrapolation, on peut demander à perf de ne mesurer
que les quantités qui nous intéressent, ici l’activité des cache CPU, en
spécifiant les événements souhaités avec l’option --event
, qui s’abbrévie en
-e
.
srun --pty \
perf stat -e L1-dcache-loads,L1-dcache-load-misses,LLC-loads,LLC-load-misses \
./scale.bin 2048 10000000
Exercice : Répétez l’expérience précédente (varier la taille du jeu de données à nombre de calculs constant), en surveillant cette fois l’activité cache.
Les métriques observées au niveau du cache L3 pourraient vous surprendre. Cependant, gardez à l’esprit que comme vous l’avez observé dans l’exercice précédent, la fréquence d’horloge du CPU diminue en situation de stress mémoire, ce qui laisse davantage de temps aux caches pour précharger spontanément des données que vous allez probablement demander par la suite. Dans ce programme simple, la prédiction de vos accès futurs est 100% efficaces.
Pour avoir une vue plus complète de l’activité des caches, vous pouvez utiliser
la liste d’événements spécifiques à Intel
mem_inst_retired.all_loads,mem_load_retired.l1_hit,mem_load_retired.l1_miss,mem_load_retired.fb_hit,mem_load_retired.l2_hit,mem_load_retired.l2_miss,mem_load_retired.l3_hit,mem_load_retired.l3_miss
.
Et pour observer ce qui se change quand vos accès mémoire ne suivent plus une
logique prévisible, essayez le programme chase.bin
, qui lit les éléments d’un
gros tableau dans un ordre aléatoire. Vous aurez probablement envie de diminuer
la charge de travail ce faisant, car c’est très inefficace.
Métriques
Nous l’avons déjà mentionné, perf stat
est capable de calculer
automatiquement certaines métriques de performance à partir des mesures brutes
que vous demandez.
Mais il est également possible de prendre le problème dans l’autre sens, et
d’indiquer les métriques que vous recherchez en laissant le soin à perf
de
sélectionner les bons événements matériels à mesurer pour les calculer. Cela se
fait avec l’option --metrics
, qui s’abbrévie en -M
.
Par exemple, si on souhaite tout savoir sur les calculs flottants effectués à chaque cycle CPU, on peut demander la métrique FLOPc (opérations flottants par cycle) :
srun --pty \
perf stat -M FLOPc \
./scale.bin 2048 10000000
Le détail des opérations flottantes qui est associé à cette métrique nous permet d’avancer dans la compréhension des faibles performances de notre exemple :
- Le code n’utilise que des opérations 256-bit, pas les opérations 512-bit natives du matériel. En effet, GCC et clang n’utilisent pas ces dernières par défaut, en raison du trop grand nombre de CPUs Intel où elles sont mal émulées par des unités de calcul 256-bit.
- Le code n’effectue qu’une opération flottante tous les deux cycles environ (rappelez vous qu’en présence d’hyperthreading, perf compte deux cycles CPU par cycle d’horloge réellement écoulé), alors qu’en principe le matériel est capable de traiter deux opérations par cycle.
Il serait un peu long d’expliquer le second problème avec juste perf stat
,
nous serons plus productifs avec perf annotate
.
Mais perf stat
suffit pour étudier le premier problème.
Exercice : Recompilez le code en activant la vectorisation 512-bit, ce que vous pouvez faire avec la commande…
rm -f scale.bin && srun make OPTS='-O3 -mprefer-vector-width=512' scale.bin
…puis répétez l’analyse précédente.
Vous noterez que l’accélération dépasse le facteur 2. C’est un phénomène
intriguant pour lequel je n’ai pas d’explication. Il semblerait que pour une
raison ou pour une autre, le CPU manie plus efficacement les instructions
vectorielles de largeur native, et ce en dépit d’une réduction de la fréquence
d’horloge que vous pouvez observer avec perf stat
sans arguments.
Autres options
Répétition
Quand on mesure des performances logicielles, il est risqué de travailler avec des mesures isolées sans barres d’erreur. On peut très facilement se retrouver à croire voir des différences de performances là où il n’y a que du bruit de mesure.
Pour éviter ce problème, je recommande d’utiliser quand c’est possible l’option
--repeat
, qui s’abbrévie en -r
et demande à perf stat
de lancer la
commande indiquée plusieurs fois en calculant la moyenne et l’écart-type de
chaque métrique affichée :
srun --pty \
perf stat -r 5 \
./scale.bin 2048 10000000
Fenêtre de temps
Sur des programmes plus complexes, on peut aussi se heurter au fait que
perf stat
n’affiche par défaut que des quantités aggrégées sur l’ensemble de
l’exécution du programme, alors que le comportement du programme varie dans le
temps. A minima, on a souvent au moins une phase d’initialisation, une phase de
calcul, et une phase de finalisation que l’on aimerait bien séparer.
Pour gérer ce genre de problème, perf stat
fournit plusieurs options utiles :
- L’option
--interval <msecs>
prend en paramètre un nombre de millisecondes, et affiche des statistiques intermédiaires toutes les N millisecondes. - L’option
--delay <msecs>
attend N millisecondes avant de commencer la collecte de données. - L’option
--timeout <msecs>
interrompt la collecte de données au bout de N millisecondes.
En combinant --delay
et --timeout
, on peut facilement sélectionner une phase
de l’exécution du programme pour l’analyser plus précisément, et si jamais le
programme n’affiche pas en standard la durée de ses phases d’exécution, l’option
--interval
peut être utilisée pour l’estimer rapidement sans modifier le code.
Système entier
L’utilisation de perf stat
n’est pas restreinte au suivi de l’utilisation des
ressources par une commande. On peut suivre toute l’activité système avec
l’option --all-cpus
, abbréviée en -a
, qui est activée par défaut si l’on
invoque perf stat
sans lui donner une commande en argument.
Dans le cadre de srv-calcul-ambulant
, ce périmètre est généralement trop large
et on souhaitera ne surveiller que l’activité de certains coeurs CPU, ce qu’on
peut faire avec l’option --cpu
, qui s’abbrévie en -C
.
Nous pouvons par exemple utiliser cela pour comparer l’activité des coeurs CPU interactifs à celle des dédiés au benchmark pendant qu’un moniteur système s’exécute sur une session interactive…
# Dans un shell
dstat-summary
# Dans un autre shell
perf stat --timeout 2000 -r 5 -C 0,1,18,19
perf stat --timeout 2000 -r 5 -C 2-17,20-35
On notera que certaines des métriques de perf
sont à interpréter avec
précautions dans ce mode (par exemple la fréquence d’horloge CPU moyenne en
présence de veille intermittente). Toutefois, l’information essentielle reste :
bien qu’étant 8x plus nombreux, les coeurs de benchmark exécutent ~6x moins
d’instructions dans ce test simple, ou ~50x moins d’instructions par coeur.
Aide intégrée
Il existe beaucoup d’autres options de perf stat
, permettant entre autres
choses de suivre l’activité d’un processus en cours d’exécution (option
-p <PID>
) et dans le cas d’un suivi à l’échelle du système entier de
ségréréger les données mesurées par coeur CPU, socket, noeud NUMA…
Pour en savoir plus sur ces fonctionnalités plus avancées, vous pouvez consulter
l’aide intégrée de perf stat
avec la commande perf help stat
.
perf annotate
Dans la première partie sur perf stat
, nous avons vu comment perf
nous
permet de compter divers événements matériels et système.
Dans cette seconde partie, nous allons maintenant aller plus loin en corrélant la survenue de ces événements avec les actions du programme, ce qui nous permettra de repérer des “points chauds” du code pour cibler notre effort d’optimisation sur ceux-ci.
Échantillonnage
Avant d’étudier perf annotate
, ou les autres outils basés sur perf record
,
il faut avoir bien compris la notion théorique de profilage par échantillonnage.
Lorsqu’on étudie la performance d’un programme, il est important de le faire de façon aussi peu intrusive que possible. Sinon, on risque de mesurer les caractéristiques de performances de son outil de mesure plutôt que celles du programme qui nous intéresse.
De ce point de vue, il n’est pas souhaitable de mesurer la façon dont un programme utilise ses cycles CPU en analysant la position du pointeur d’instruction (= le point du programme où on se trouve) à chaque cycle CPU. En effet, analyser la position du pointeur d’instruction (ou même juste l’écrire quelque part) demande plusieurs cycles CPU, pendant lesquels le programme ne pourrait pas avancer sous peine de devancer le profileur. Donc cela conduirait à ralentir démesurément l’exécution du programme, en nous éloignant d’autant d’une exécution réaliste. Ainsi, un programme limité par les entrées-sorties “dans la vraie vie” pourrait apparaître limité par la vitesse d’exécution de code dans les mesures.
De même, quand on s’intéresse à l’utilisation du cache CPU, il ne serait pas souhaitable d’enregistrer des informations à propos de chaque défaut de cache L1, car cela arrive aussi bien trop souvent.
A la place, tous les profileurs modernes utilisent une approche statistique appelée échantillonnage, où l’on n’enregistre qu’un sous-ensemble des événements qui nous intéressent (N événements par seconde, ou bien 1 événement tous les N événements), appelés échantillons.
L’idée de cette approche est que si notre jeu d’échantillons est représentatif et de taille suffisante, nous pouvons en conclure à peu près les mêmes choses que nous aurions conclues si nous avions eu le loisir d’enregistrer et analyser la totalité des événements. Par exemple, si nous avons mesuré un grand nombre d’échantillons et 20% d’entre eux tombent dans une certaine partie du code, alors nous pouvons supposer que si nous avions pu mesurer la totalité des événements étudiés, environ 20% se seraient produits dans cette même partie du code.
Une originalité de perf
par rapport à d’autres profileurs fonctionnant
par échantillonnage est que ce profileur n’est pas seulement capable de mesurer
quelles sont les parties du code où l’on passe beaucoup de cycles CPU, mais
plus généralement quelles sont les parties du code qui sont corrélées à toutes
sortes d’événements système : défauts de cache, écritures disque, etc.
Code annoté
Pour enregistrer un profil par échantillons avec perf
, on utilise la commande
perf record
.
Dans sa configuration par défaut, perf record
enregistre le pointeur
d’instruction à intervalle de temps régulier (plus précisément à intervalle de
cycles CPU régulier). Cela permet d’analyser dans quelles parties du code l’on
passe le plus de temps. Essayons :
srun --pty \
perf record \
./scale.bin 2048 10000000
Vous noterez que contrairement à perf stat
, perf record
n’émet aucun
résultat de mesure en sortie, il mentionne juste que des données ont été écrites
dans un fichier appelé perf.data
.
Nous pouvons ensuite afficher l’assembleur du programme, avec des annotations
indiquant la proportion du temps passé à exécuter chaque instruction, en
utilisant perf annotate
…
perf annotate
…et nous constatons que sur les 814 lignes que produit le désassembleur
objdump -d
pour ce programme, 92% du temps CPU est passé à exécuter
quatre instructions assembleur. Il s’agit de notre fond de boucle de
multiplication de flottants, comme le clarifie la ligne de code source associée.
perf
a pu retrouver le code source associé à l’assembleur grâce aux symboles
de déboguage, le programme ayant été compilé avec l’option -g
.
Par défaut, perf annotate
place le curseur sur l’instruction assembleur la
plus “chaude” du point de vue de l’événement considéré (ici le passage des
cycles processeurs). On peut aller vers les instructions de moins en moins
“chaudes” en utilisant TAB (avec retour en arrière via Maj+TAB).
Une autre commande clavier à connaître est la touche t
, qui permet de basculer
de l’affichage par défaut indiquant le pourcentage des échantillons
correspondant à chaque ligne de code à d’autres affichags décrivant les données
brutes. Parmi ces affichages, l’affichage “Samples” indique le nombre
d’échantillons mesurés sur chaque ligne de code, ce qui permet de s’assurer
qu’on n’est pas en train de tirer des conclusions hâtives d’une mesure non
statistiquement significative.
Deux autres touches qu’il est utile de connaître sont la touche q
qui permet
de quitter l’interface perf annotate
, et la touche h
qui permet d’afficher
l’aide des commandes claviers.
Si l’on place le curseur sur une instruction de saut, perf annotate
va
indiquer la cible du saut via une flèche, ainsi que la condition associée si il
s’agit d’un saut conditionnel. Si l’on appuie ensuite sur Entrée, le curseur
sera déplacé à la cible du saut. Nous retrouvons ainsi notre boucle de calcul :
Et avec ce zoom sur l’assembleur qui limite les performances nous pouvons maintenant répondre à une des questions de performances que nous nous posions précédemment : pourquoi ce programme ne fait-il qu’une multiplication vectorisée tous les deux cycles ?
Pour le profane de l’assembleur, ce code stocke…
- La base du tableau “input” dans le registre rdx
- La base du tableau “output” dans le registre rax
- Le multiplicateur 4.2f, répété pour remplir un registre vectoriel, dans zmm2
- Le compteur de boucle (en octets) dans le registre rcx
- La taille de tableau (en octets) dans le registre rdi
A chaque itération, il…
- Charge un paquet de flottants depuis la position actuelle du tableau “input”…
- …le multiplie par notre paquet de multiplicateurs zmm2
- …et stocke le résultat dans zmm0
- Stocke le contenu de zmm0 à la position actuelle du tableau “output”.
- Incrémente le compteur de boucle.
- Regarde si on est arrivé à la fin des tableaux.
- Sinon, recommence du début
Ces opérations sont, en principe, exécutables en parallèle par un CPU moderne utilisant des mécanismes de pipelining, prédiction de branchement et spéculation. En simplifiant, un CPU peut, pendant les premiers cycles…
- Charger un 1er paquet de flottants, calculer le 2e compteur de boucle, spéculer que la boucle va continuer pour une 2e itération.
- Charger un 2e paquet de flottants, multiplier les 1ers flottants par le multiplicateur, calculer le 3e compteur de boucle, spéculer que la boucle va continuer pour une 3e itération, vérifier que la spéculation sur le 2e compteur de boucle était correcte.
- Charger un 3e paquet de flottants, multiplier les 2èmes flottants par le multiplicateur, stocker les 1ers résultats, calculer le 4e compteur de boucle, spéculer que la boucle va continuer pour une 4e itération, vérifier que la spéculation sur le 3e compteur de boucle était correcte..
- …et ainsi de suite en régime établi…
Mais il y a toutes sortes de limites pratiques à ce parallélisme d’instructions. L’une d’elle est que notre CPU (Intel i9-10980XE) ne peut pas traiter de façon pérenne plus de 4 opérations élémentaires par cycle, où dans notre exemple, les opérations élémentaires sont…
- Une opération mémoire (lecture ou écriture)
- Une operation arithmétique (vectorielle ou scalaire)
- La paire d’opérations comparaison + saut conditionnel
Pour plus de détails sur ce point, vous pourrez consulter le manuel de microarchitecture d’Agner Fog ainsi que l’entrée de Wikichip concernant la microarchitecture Skylake après le TP.
Toujours est-il que nous dépassons ici cette limite de 4 opérations par cycle avant d’avoir atteint la limite de performances arithmétique de 2 multiplications par cycle à cause des opérations de gestion du compteur de boucle. Heureusement, ce n’est pas une fatalité, et nous pouvons amortir le coût de ces opérations en déroulant la boucle, c’est à dire en faisant plusieurs multiplications vectorielles par saut au niveau de l’assembleur.
Pour une raison inconnue, GCC n’a pas effectué cette optimisation spontanément
ici, mais on peut l’encourager avec l’option de compilation -funroll-loops
.
Exercice : Recompilez le programme avec cette option, tout en conservant la vectorisation 512-bits, ce que vous pouvez faire avec la commande…
rm -f scale.bin \
&& srun make OPTS='-O3 -mprefer-vector-width=512 -funroll-loops' scale.bin
…puis analysez l’effet sur l’assembleur généré et les performances avec
perf stat
et perf annotate
.
Aller plus loin
Conclusions sur le premier exemple
Au terme de l’exercice qui précède, vous devriez être arrivé à une performance de calcul d’un peu moins d’une multiplication 512 bits par cycle.
Un peu moins car à l’heure où ces lignes sont écrites, sur
srv-calcul-ambulant
, on calcule en réalité deux multiplications en trois
cycles. Cela est dû à un problème d’exécution superscalaire appelé “4K aliasing”,
dont vous pouvez surveiller l’émergence avec le compteur de performance Intel
ld_blocks_partial.address_alias
, à considérer en proportion du compteur de
chargements mémoire mem_inst_retired.all_loads
.
Pour mieux comprendre ce qui se passe, considérons la partie chaude de notre boucle de calcul tel que le CPU la voit, donc au niveau de l’assembleur généré :
vmulps (%rdi,%rax,1),%zmm0,%zmm10
vmovups %zmm10,(%r15,%rax,1)
vmulps 0x40(%rdi,%rax,1),%zmm0,%zmm11
vmovups %zmm11,0x40(%r15,%rax,1)
vmulps 0x80(%rdi,%rax,1),%zmm0,%zmm12
vmovups %zmm12,0x80(%r15,%rax,1)
vmulps 0xc0(%rdi,%rax,1),%zmm0,%zmm13
vmovups %zmm13,0xc0(%r15,%rax,1)
vmulps 0x100(%rdi,%rax,1),%zmm0,%zmm14
vmovups %zmm14,0x100(%r15,%rax,1)
vmulps 0x140(%rdi,%rax,1),%zmm0,%zmm15
vmovups %zmm15,0x140(%r15,%rax,1)
vmulps 0x180(%rdi,%rax,1),%zmm0,%zmm2
vmovups %zmm2,0x180(%r15,%rax,1)
vmulps 0x1c0(%rdi,%rax,1),%zmm0,%zmm4
vmovups %zmm4,0x1c0(%r15,%rax,1)
add $0x200,%rax
cmp %rax,%r8
jne 401918 <main._omp_fn.0+0x438>
A chaque étape du calcul, nous lisons des données depuis une adresse mémoire, calculons des résultats, et stockons les résultats à une autre adresse mémoire. Et pour travailler de façon efficace, le CPU voudrait exécuter l’ensemble de ces opérations en parallèle avec une organisation en pipeline : à chaque cycle d’horloge, il doit pouvoir simultanément lire des données pour une itération de boucle N, faire le calcul pour l’itération précédente N-1, et stocker le résultat de l’itération N-2.
Mais ce parallélisme d’instructions opère sous une contrainte importante : il doit produire le même résultat que si le programme avait été exécuté séquentiellement, une instruction à la fois. Et ce même dans le cas tordu où on spécifierait des tableaux d’entrée et de sortie qui se recouvrent.
Dans cette situation, si l’adresse d’écriture à l’itération N, que nous noterons , correspond à une adresse de lecture pour une itération M > N future de la boucle de calcul, alors cette itération M de la boucle devra attendre que le résultat de l’itération N soit prêt pour s’exécuter. Ce n’est pas un problème si M est beaucoup plus grand que N, car le résultat de l’itération N sera prêt au moment où l’itération M en aura besoin. Mais c’est un problème si M est juste un peu plus grand que N, car cela crée une chaîne de dépendances qui empêche l’exécution parallèle.
Plus formellement, dans notre cas où nous faisons des écritures et lectures mémoire de même taille et bien alignées, ce qui permet d’écarter la question des recouvrements partiel, et où nous avons un pipeline de calcul de longueur 3 (lecture, multiplication, écriture), il y a un problème quand on se retrouve dans la situation avec petit.
Très bien, me direz-vous, mais pourquoi est-ce que je vous parle de ce cas tordu alors qu’il ne nous concerne clairement pas ici, vu que nous écrivons du code civilisé où les tableaux d’entrée et de sortie ne se recouvrent pas ? Eh bien parce que le CPU ne peut pas malheureusement pas vérifier cette propriété de façon exacte.
En effet, les CPU x86 disposent d’un mécanisme de mémoire virtuelle qui permet à deux pointeurs de valeurs différentes de correspondre à la même adresse physique en RAM. Pour éliminer cette possibilité, il faudrait consulter la table des traductions d’adresse, et cela prendrait bien trop de temps pour notre pauvre ordonnanceur superscalaire qui doit donner son verdict en quelques cycles. Une approximation est donc nécessaire ici.
L’approximation qui est faite, c’est de n’utiliser pour étudier la possibilité d’un recouvrement que les 12 bits de poids faible des adresses mémoire concernées, ceux-ci n’étant pas affectés par le processus de traduction d’adresse sous architecture x86 (qui travaille par blocs alignés de 4 Ko).
Nous ne voulons donc pas seulement avoir . Nous voulons en fait avoir avec aussi petit que possible sans être nul. Comme nous travaillons en AVX-512, où nos accès mémoire se font par blocs consécutifs de 64 octets, cela revient à dire qu’on veut avoir…
Il n’est pas facile de garantir ce genre de propriété sur les adresses mémoires de nos tableaux d’entrée et de sortie quand ils s’agit de deux allocations mémoire séparées dont le placement en mémoire est laissé à la discrétion de l’implémentation de la bibliothèque standard C. Mais on peut se placer dans cette situation en n’allouant qu’un seul gros tableau dont le début servira pour les sorties et la fin pour les entrées, avec un petit peu de padding au milieu pour atteindre l’espacement souhaité entre données d’entrée et de sortie :
const size_t aliasing_stride = 4096 / sizeof(float);
const size_t natural_alignment = amount % aliasing_stride;
const size_t desired_alignment = 192 / sizeof(float);
const size_t padding = (natural_alignment < desired_alignment)
? desired_alignment - natural_alignment
: aliasing_stride - natural_alignment + desired_alignment;
//
simd_friendly_vector<float> buffer(2*amount+padding, 0.0);
float* const output = buffer.data();
const float* const input = output + amount + padding;
for (size_t iter = 0; iter < iterations; ++iter) {
assumeAccessed(input);
for (size_t i = 0; i < amount; ++i) {
output[i] = input[i] * 4.2f;
}
assumeAccessed(output);
}
Et ceci étant fait, on arrive bien à une multiplication 512-bit par cycle, ce qui est déjà nettement mieux que notre point de départ. Mais au début du TP, j’avais mentionné que notre processeur peut effectuer deux multiplications par cycle. Il nous reste donc encore un facteur 2 de puissance de calcul flottante au niveau du CPU qui n’est pas exploitée par ce programme.
La raison est que notre processeur Cascade Lake ne peut faire qu’une écriture mémoire par cycle processeur. Or nous effectuons une écriture mémoire pour chaque calcul que nous faisons. Nous ne pouvons donc pas effectuer plus d’un calcul par cycle pour ce benchmark.
Donc si on veut des performances de calcul optimales de façon portable, une des contraintes qu’on doit respecter est de faire davantage de calculs par écriture en mémoire.
Pour une petit liste d’autres limites possible à la vitesse d’exécution d’un programme calculatoire, vous pourrez consulter https://travisdowns.github.io/blog/2019/06/11/speed-limits.html après le TP.
Exercice avancé
Si vous êtes en avance et êtes plutôt à l’aise en lecture d’assembleur x86,
voici un second exercice qui vous permettra de mieux apprécier les possibilités
offertes par perf annotate
.
Le programme sum.cpp
, qui calcule la somme des éléments d’un tableau de
nombres flottants, n’est pas limité par la performance des écritures mémoire. On
s’attendrait donc à ce qu’il atteigne une performance 2x plus importante que
scale.cpp
une fois pleinement optimisé.
Toutefois, dans son état actuel, ce programme s’exécute un peu moins de 13x plus
lentement que la version initiale de scale.cpp
!
Si l’on passe l’option -ffast-math
à GCC, qui autorise des optimisations
numériquement instables, le programme s’exécute plus rapidement. Mais pas encore
aux performances crêtes du matériel.
Avec l’aide des outils que nous avons vus jusqu’à présent, essayez de comprendre par vous-même…
- Pourquoi les performances du programme de base sont aussi mauvaises.
- Quels changements surviennent dans le code généré lorsque l’option
-ffast-math
est passée à GCC, et pour quelle raison ces optimisations ne sont pas permises sans cette option.
Ayant éclairci ces points, vous pouvez ensuite essayer de terminer le travail d’optimisation incomplètement effectué par GCC en atteignant les performances crêtes du matériel. Pour cela, vous aurez besoin des deux informations microarchitecturales suivantes :
- Notre microarchitecture CPU ne peut effectuer qu’un seul saut par cycle.
- Deux opérations arithmétiques qui dépendent les unes des autres (ex : le résultat de la première opération est passé en paramètre de la seconde opération) ne peuvent pas être s’exécuter en parallèle.
Si vous souhaitez “pimenter” un peu l’exercice, essayez d’atteindre les
performances crêtes du matériel sans avoir recours ni à -ffast-math
, ni à des
intrinsèques spécifiques à l’architecture x86.
Autres options
perf annotate
a relativement peu d’options intéressantes, on notera surtout
quelques options de filtrage qui peuvent parfois être utiles comme…
--dsos
/-d
qui permet de n’afficher que le source d’un binaire sur des programmes composés de plusieurs binaires liés dynamiquement--symbol
/-s
qui permet de n’afficher qu’une seule fonction (symbole) du binaire cible--cpu
/-C
qui permet de n’afficher que l’activité de certains coeurs CPU
En revanche, perf record
possède un grand nombre d’options qui sont souvent
utiles. Nous n’aborderons certaines que dans
la partie sur perf report
, où leur fonction sera
plus claire, mais nous pouvons déjà en mentionner quelques unes ici.
Similarités avec perf stat
Tout d’abord, un grand nombre d’options que nous avons déjà abordées dans le
cadre de perf stat
restent utilisables avec perf record
. C’est ainsi le
cas de --event
/-e
, --all-cpus
/-a
, --pid
/-p
, --cpu
/-C
et
--delay
/-D
. Comme nous avons déjà abordé ces options dans la partie sur
perf stat
, nous ne reviendrons pas dessus ici.
Fréquence d’échantillonage
L’un des plus importants compromis de configuration d’un profileur par échantillonnage est la fréquence avec laquelle il mesure des échantillons, sa fréquence d’échantillonage :
- Plus l’on mesure des échantillons fréquemment, plus la mesure est précise à temps de mesure égal, mais plus l’on biaise le profil de performance du programme étudié en consommant des ressources systèmes au niveau du profileur.
- La fréquence d’échantillonage ne doit pas être multiple d’une fréquence temporelle caractéristique à laquelle le programme effectue des tâches, sous peine de sur ou sous-évaluer aléatoirement le poids de ces tâches dans l’exécution du programme.
Comme le coût de mesure d’un échantillon peut varier grandement en fonction de
ce qu’on mesure, il est préférable d’ajuster empiriquement la fréquence
d’échantillonnage. Un moyen de le faire est de mesurer une caractéristique de
performance simple du programme (par exemple le temps d’exécution) avec et sans
instrumentation perf record
, et de réduire la fréquence d’échantillonnage de
perf record
jusqu’à ce que l’on n’observe plus de dégradation de
performance mesurable.
Perf permet d’ajuster la fréquence d’échantillonnage selon deux critères :
- Mesurer 1 événement tous les N événements, via l’option
--count N
qui s’abbrévie en-c N
. C’est en général le levier nativement utilisé par l’implémentation deperf
sous le capot. Son principal défaut est que le surcoût associé à la mesure dépend de la fréquence de l’événement, qui n’est a priori pas connue à l’avance et variable dans le temps. - Viser une fréquence d’échantillonnage moyenne de M échantillons par seconde,
via l’option
--freq M
qui s’abbrévie en-F M
. Cette option offre des surcoûts de mesure mieux contrôlés, et est donc généralement préférable.
Pour ne pas bloquer le système, perf
s’impose une limite de fréquence
d’échantillonnage. Si l’on demande une fréquence plus élevée, la demande sera
ignorée. On peut demander à perf
d’opérer à cette fréquence maximale avec
-F max
. Cette limite est configurable, comme expliqué dans
l’annexe sur l’installation de perf
.
Fichier de sortie
perf record
produit par défaut un fichier de données dans le répertoire
courant, ce qui occasionne logiquement des écritures sur le périphérique associé.
Cela peut être problématique dans deux cas :
- Quand on analyse les performances d’un programme qui utilise le même
périphérique, car l’activité de stockage associée à
perf
va se mélanger à celle du programme étudié et rendre les mesures plus difficiles à interpréter. - Quand on pousse
perf
dans ses retranchements et lui fait générer un grand flux de données, plus intense que le périphérique de stockage sous jacent ne peut absorber.
Pour gérer ces cas, on peut indiquer à perf record
d’écrire ses données à un
autre endroit avec l’option --output /chemin/vers/le/fichier
, qui s’abbrévie
en -o /chemin/vers/le/fichier
. Cette option doit être couplée à une option
--input
équivalente dans les commandes qui analysent la sortie de
perf record
, comme perf annotate
.
En particulier, une astuce courante est d’écrire les données dans un ramdisk, répertoire virtuel hébergé en RAM et qui dispose donc d’une bande passante très importante que le périphérique de stockage. En fin de mesure, on ramènera ensuite les données dans le répertoire courant pour ne pas encombrer inutilement la RAM et risquer de perdre ses données en cas d’arrêt de la machine.
Sur srv-calcul-ambulant
, on peut le faire comme ceci :
srun --pty \
perf record -o /dev/shm/${USER}/perf.data ... autres options ... \
&& mv /dev/shm/${USER}/perf.data .
perf list
Dans les premières parties, vous avez travaillé pour faire simple avec des
événements choisis automatiquement par perf
ou donnés par l’énoncé du TP.
Dans cette partie, nous allons utiliser la commande perf list
pour explorer
la liste complète des événements système prédéfinis par perf
.
Événements prédéfinis
Pour avoir la liste complète des événements système prédéfinis par perf
,
utilisables via l’option --event
(qui s’abbrévie en -e
) de toutes les
commandes perf
qui font un suivi d’événements, vous pouvez utiliser la
commande perf list
.
Mais comme vous allez rapidement le constater si vous essayez, cette liste est très longue (5933 lignes au moment où j’écris ce TP). Il est donc utile de connaître ses mécanismes de filtrage.
Tout d’abord, les événements sont triés en grandes catégories, que j’aurais personnellement tendance à regrouper encore en catégories de niveau supérieur :
- Avec
perf list hw cache
, vous pouvez afficher la liste des événements matériels génériques/abstraits. Ces événements sont mesurables sur à peu près tout matériel supporté parperf
, et implémentés en utilisant les événements spécifiques à chaque matériel. - Avec
perf list sw tracepoint sdt
, vous pouvez afficher la liste des événements logiciels, qui sont générés par le noyau Linux et certaines bibliothèques. Au niveau des sous-catégories…- Ceux libellés
[Software event]
et[Tool event]
sont accessibles sur tout ordinateur équipé deperf
sans configuration supplémentaire. - Ceux libellés
[Tracepoint event]
et[SDT event]
nécessitent des privilèges de traçage (voir l’annexe installation pour plus d’informations sur ce point), qui ont été préconfigurés pour vous sursrv-calcul-ambulant
.- Ceux libellés
[SDT event]
nécessitent également des préparatifs avant utilisation, que nous aborderons dans la partie surperf probe
.
- Ceux libellés
- Ceux libellés
- Avec
perf list pmu
, vous pouvez afficher la liste des événements que votre CPU sait compter via sa Performance Monitoring Unit (PMU). Ils sont très nombreux, particulièrement chez Intel, c’est la majeure partie de la sortie brute deperf list
sans filtrage. - Avec
perf list metric metricgroup
, vous pouvez afficher la liste des métriques et groupes de métriques. Mais curieusement, cette liste n’est pas hiérarchisée comme la sortie finale deperf list
, ce qui la rend moins lisible. Je vous recommande donc plutôt d’utiliserperf list
sans arguments ici, en sautant à la fin du texte avec la toucheFin
de votre clavier.
Il est aussi possible d’effectuer une recherche au sein de la sortie de
perf list
, par exemple perf list float
retournera tous les événements dont
le nom ou la description contient le mot “float”.
Exercice : Quand on vectorise du code sans revoir sa politique d’allocation mémoire, on se retrouve fréquemment avec des problèmes de performances liés à des accès mémoire non alignés.
Contrairement à une croyance populaire tenace, les mauvaises performances
observées ne proviennent pas du fait que le compilateur génère des instructions
non optimisées pour les accès alignés comme movups
. Sur les CPUs actuels,
ces instructions ont des performances équivalentes à leurs cousines optimisées
pour les accès alignés, qui sont donc plus ou moins obsolètes.
En réalité, le vrai problème est que dans cette configuration, les accès mémoire liés au calcul vectorisé, qui sont très larges, peuvent et vont souvent se retrouver à cheval sur deux lignes de cache, ce qui force le processeur à lire ou écrire deux lignes de cache au lieu d’une. Il en résulte une sur-utilisation des ressources mémoire, qui cause le ralentissement observé.
Dans la section “cache” de la sortie de perf list pmu
, trouvez quels
événements passer en paramètre à perf stat -e
pour établir avec certitude si
vous avez ou non affaire à ce problème.
Modificateurs
Il est possible d’appliquer un certain nombre de contraintes à la façon dont
perf
enregistre des événements, par le biais de suffixes placés à la fin du
nom de l’événement, après un signe :
(par exemple L1-dcache-load-misses:u
).
- Le suffixe
k
permet de ne compter que les événements survenus pendant l’exécution du code du noyau Linux, et de façon symétrique le suffixeu
permet de ne compter que les événements survenus hors du noyau (en user-space). - Le suffixe
p
peut être spécifié une ou plusieurs fois, pour indiquer avec quelle précision on souhaite que les événements soient localisés dans le code source du programme. Ceci concerne les mesures à base de PMU, pour lesquels une localisation parfaite des événements dans le code source peut être onéreuse voire impossible au niveau matériel à cause du parallélisme d’instructions.- Si ce suffixe n’est pas précisé, les événements peuvent être détectés à
une distance arbitraire et variable du point où le programme se trouvait
réellement quand ils se sont produit. L’interprétation de l’assembleur
annoté (
perf annotate
) et du profil de fonctions courtes doit dans ce cas être effectuée avec de grandes précautions ! - Si il est précisé une fois (comme dans
L1-dcache-load-misses:p
), les événements peuvent être détectés à distance du point du code source dont ils sont originaires, mais avec un décalage en instructions constant. Il suffit donc de trouver quel est ce décalage pour interpréter correctement l’assembleur annoté. - Si il est précisé deux fois (comme dans
L1-dcache-load-misses:pp
),perf
demande en plus au CPU de viser un décalage en instructions nul. La demande sera ignorée si le CPU ne peut pas la satisfaire. - Si il est précisé trois fois (comme dans
L1-dcache-load-misses:ppp
), le CPU émulera un décalage en instructions nul si nécessaire en randomisant la position détectée du pointeur d’instruction. Attention, ce mode nécessite davantage de travail que le mode:pp
du côtéperf
(il faut donc revoir les fréquences d’échantillonage à la baisse), et il n’est pas disponible pour tous les compteurs ni tous les fabricants de CPUs.
- Si ce suffixe n’est pas précisé, les événements peuvent être détectés à
une distance arbitraire et variable du point où le programme se trouvait
réellement quand ils se sont produit. L’interprétation de l’assembleur
annoté (
- Le suffixe
P
représente vers la forme la plus précise du suffixep
disponible sur le CPU hôte.
Motifs wildcards
Les événements de type [Tracepoint event]
(et seulement eux) supportent le
motif wildcard *
avec les mêmes sémantiques que bash
. On peut demander à
une commande comme perf stat
de mesurer tous les événements qui respectent un
tel motif.
Par exemple, on peut surveiller pendant 10s les appels système ayant trait à l’horloge du noyau…
perf stat --timeout 10000 -e syscalls:*clock*
…et l’on constatera que l’usage le plus fréquente que les applications font de l’horloge système est de loin d’attendre qu’une certaine durée soit écoulée.
Exercice : Selon le même principe, mesurez quels sont les tracepoint
liés au réseau (groupe net:
) qui sont visités le plus fréquemment.
perf trace
Dans la partie précédente, nous avons rappelé que perf
est capable de mesurer
diverses formes d’activité du noyau Linux, et montré comment compter ces
événements avec perf stat
.
Toutefois, quand on analyse des composants logiciels complexes, un simple décompte d’événements ne suffit pas toujours. On veut souvent connaître d’autres choses comme…
- Les informations qui passées en paramètre d’une API, ou retournées par celle-ci.
- L’enchaînement temporel des opérations étudiées.
perf trace
est un outil qui aide à répondre ce type de questions.
Commande unique
Une première utilisation possible de perf trace
est de surveiller les appels
systèmes effectués par une commande, à la manière de l’utilitaire Unix
traditionnel strace
:
srun --pty \
perf trace \
./scale.bin 2048 10000000
Sortie de perf trace
Pour comparaison, voici la sortie de strace
sur ce même programme :
Sortie de strace
Que peut-on conclure de cette première observation ?
On constate tout d’abord que même l’exécution d’un “pur calcul” nécessite des appels systèmes, liés au chargement des bibliothèques partagées et à diverses étapes d’initialisation.
On remarquera aussi que la sortie de perf trace
est moins verbeuse que celle
de strace
, car elle omet certains détails comme les nombreuses étapes de
recherche des bibliothèques partagées. Selon ce que l’on essaie de faire, cela
peut être un avantage ou un inconvénient.
On notera enfin que perf trace
est dans l’ensemble un outil moins mature que
strace
, et qu’il lui manque notamment cruellement la capacité de décoder les
arguments de type chaîne de caractère.
Mais ce qui est moins visible, c’est que l’implémentation de ces deux outils est aussi très différente :
strace
fonctionne comme un débogueur : il demande au noyau Linux d’interrompre le programme et lui donner la main à chaque fois qu’un appel système survient, affiche les caractéristiques de l’appel système observé, puis fait redémarrer le programme.perf trace
n’interrompt pas l’exécution du programme, il demande plutôt au noyau de lui envoyer une notification chaque fois qu’un appel système survient et affiche ensuite les événements dont il a été notifié de façon asynchrone.
Ces différences d’implémentation ont plusieurs conséquences :
perf trace
a un impact bien moins élevé sur les performances du programme étudié.- La sortie de
perf trace
n’est pas synchronisée avec celle du programme étudié, ce qui peut parfois la rendre plus difficile à interpréter. - Si le programme effectue des appels système plus vite que
perf trace
ne parvient à les analyser,perf trace
ne parviendra pas à traiter tous les appels système. perf trace
n’est pas restreint à la surveillance des seuls appels système et peut surveiller l’activité du système entier, intérieur du noyau Linux compris.
Exercice : Comparez perf trace
et strace
en termes de comportement et
d’impact sur les performances de la commande tree /usr >/dev/null
, qui énumère
l’ensemble des fichiers du dossier /usr
et effectue pour cela un très grand
nombre d’appels système. Voici une commande Slurm que vous pouvez utiliser comme
base de travail :
srun --pty \
bash -c 'time tree /usr >/dev/null'
N’oubliez pas de lancer la commande une première fois avant les mesures pour pré-remplir le cache disque. Du fait de l’existence de ce cache, la première exécution aura des caractéristiques de performances différentes des suivantes…
Si vous appliquez strace
au processus bash
plutôt qu’au processus tree
,
vous aurez besoin de l’option -f
qui permet de suivre les processus enfants.
perf trace
effectue ce suivi par défaut.
Système entier
Avertissement : La version de
perf
actuellement installée sursrv-calcul-ambulant
a des difficultés à nommer les threads secondaires du démonslurmctld
, qui sont juste identifiés par un identifiant numérique du genre:2301
. Il faut lancerperf trace
en tant qu’administrateur pour que les noms soient résolus correctement. Ce comportement n’était pas observé avec d’anciennes versions deperf
, et il est possible qu’il disparaisse à nouveau à l’avenir, mais en attendant, je garde les captures d’écran de l’ancienne version, qui sont plus informatives. Donc ne vous étonnez pas si vos propres exécutions deperf stat
ont des sorties texte différentes et moins claires.
Introduction
Nous l’avons mentionné précédemment, perf trace
ne représente pas une avancée
majeure par rapport à strace
pour suivre les appels système d’un processus
unique. Son intérêt principal par rapport à strace
est de donner accès à des
informations inaccessibles via ce dernier :
- L’activité du système entier
- L’activité noyau à grain plus fin que les appels système
Commençons par le suivi de l’activité du système entier. Celui-ci, qui peut être
déclenché avec le flag --all-cpus
, qui s’abbrévie en -a
, peut être riche en
enseignement. Mais il y a certaines précautions à prendre quand on a recours à
cette approche.
Pour comprendre pourquoi, essayons d’abord de le faire naïvement pendant un bref
instant. Notez au passage l’utilisation d’une commande sleep
pour limiter la
durée du suivi, cette astuce est applicable à de nombreuses commandes perf
.
srun --pty \
perf trace -a \
sleep 0.001
Sortie de perf trace
Vous ne vous attendiez peut être pas à une sortie aussi volumineuse en surveillant l’activité d’un système Linux minimaliste pendant une milliseconde. Et en effet, comme ma formulation précédente vous l’aura fait deviner, quelque chose s’est mal passé.
Un examen rapide de la sortie de perf trace
permettra de comprendre la nature
du problème. En agissant trop naïvement, nous avons violé une règle d’or du
suivi de l’activité système :
Tu ne provoqueras pas, une fois par événement système surveillé, un événement système que tu surveilles également.
Ici, à chaque fois qu’un appel système est observé, perf trace
écrit une
sortie dans la console. Cette écriture console nécessite un appel système,
lui-même détecté par perf trace
, ce qui déclenche une autre écriture console,
et donc un autre appel système… Une boucle infinie s’amorce, générant un
énorme volume d’activité système qui masquera tout ce qu’on aurait pu vouloir
observer d’autre que l’activité de perf
.
Vous devrez garder en tête ce principe à chaque fois que vous utiliserez les
fonctionnalités de perf
pour surveiller l’activité système de façon
exhaustive. Le piège ci-dessus peut vous sembler grossier après coup, mais
parfois il sera plus subtil, par exemple si vous surveillez l’activité disque ou
système de fichier avec une commande comme perf record
qui génère de
l’activité disque.
Mais dans notre cas simple d’un suivi global des appels système, perf trace
fournit plusieurs manières de contourner le problème.
Approche par résumé et détails ciblés
Une première approche est de commencer par demander un résumé des appels système
observés pendant une certaine durée avec l’option --summary
, qui s’abbrévie en
-s
et désactive la sortie “live” de perf trace
…
srun --pty \
perf trace -a -s \
sleep 5
Sortie de perf trace
…puis de relancer perf trace
en ne suivant que les appels système qui,
d’après le résumé, sont appelés particulièrement souvent ou bloquent
l’application pour une durée particulièrement longue. On peut effectuer ce
filtrage avec l’habituelle option --event
/-e
:
srun --pty \
perf trace -a -e futex,clock_nanosleep,epoll_wait \
sleep 3
Sortie de perf trace
Nous remarquons ainsi plusieurs choses intéressantes :
- Le démon
slurmctld
(un des composants de l’implémentation de Slurm) semble avoir une stratégie de synchronisation basée sur l’attente active, car il fait desclock_nanosleep
de 100ms avec une régularité de métronome, et ne fait aucun autre appel système aussi fréquemment. Il est probable qu’à cet intervalle de temps, il communique avec d’autres composants de Slurm par un canal autre que le noyau Linux, peut-être des opérations atomiques en mémoire partagée. - Le démon
sstate
(un autre composant de Slurm) semble utiliser une stratégie extrêmement agressive pour synchroniser ses threads. En effet, toutes les secondes, il fait des salves extrêmement rapides et prolongées d’appels àfutex()
, la brique de base d’implémentation des primitives de synchronisationpthread
sous Linux. Deux futex sont utilisés:- L’un d’eux, situé à l’adresse mémoire 0x55558d761da0, est utilisé pour réveiller un thread toutes les 20µs. Cette fréquence semble étonamment élevée.
- L’autre, situé à l’adresse mémoire 0x55558d761d8c, est utilisé pour
attendre une notification qui ne vient pas, encore une fois toutes les
20µs. Il semblerait qu’un timeout soit utilisé, mais vu la fréquence
d’appel celui-ci est probablement trop court pour servir à quelque chose.
Cependant, de temps en temps, un timeout plus long de 1s est utilisé.
On voit que le pointeur vers la structure
timespec
associé est toujours le même, ce qui suggère qu’elle a été modifiée en place, mais malheureusementperf trace
ne déréférence pas les pointeurs donc on ne peut pas en savoir plus sur les valeurs de timeout utilisées avec cet outil-là.
- Il arrive occasionnellement à
perf trace
de se tromper sur la durée d’un appel système et sortir une valeur franchement aberrante. Je n’ai pas d’explication à ce phénomène à l’heure actuelle, je constate juste qu’il est heureusement assez rare pour peu gêner en pratique.
Cette approche d’analyse d’activité système (combiner un premier résumé des appels systèmes avec une étude plus détaillée de ceux qui semblent intéressants) fonctionne bien tant que le phénomène que nous cherchons à observer…
- Est reproductible ou de longue durée (pour avoir le temps de faire deux
appels à
perf trace
) - N’implique pas un des appels système utilisés par
perf trace
.
Exercice : L’option --summary
/-s
peut aussi être intéressante pour
faire une analyse “haut niveau” de l’activité des commandes/processus
individuels.
Reprenez la commande tree /usr >/dev/null
étudiée plus haut, et utilisez cette
option pour faire un résumé de ses appels système. Remarquez la plus grande
clarté du résultat, mais aussi l’impact moins grand de perf stat
sur les
performances de la commande dans cette configuration.
Vous pouvez aussi tester et comparer l’équivalent strace
de cette option,
strace -c
. Si vous appliquez les deux utilitaires au processus bash
(avec
l’option -f
pour strace
), vous constaterez que leur comportement diffère un
peu : perf trace
fournit des statistiques pour chaque processus, alors que
strace
ne fournit qu’un total accumulé sur tous les processus enfants.
Approche par spécialisation des CPUs
Cette seconde approche nécessite un peu plus de préparation, mais elle vous
offrira en contrepartie une sortie plus “épurée” et focalisée sur l’information
que vous recherchez, ainsi qu’un plus faible impact de perf trace
sur les
performances des programmes étudiés.
L’idée est d’utiliser un outil comme taskset
pour isoler les processus que
vous voulez étudier sur certains coeurs CPU dans un premier temps, puis pour
lancer perf trace
sur d’autres coeurs CPU dans un second temps, en lui
indiquant de ne surveiller que les coeurs CPU où les tâches isolées se trouvent.
Cela peut être fait avec l’option --cpu
/-C
, comme dans perf stat
.
Sur srv-calcul-ambulant
, une partie du travail a déjà été fait pour vous,
puisque les tâches interactives et services systèmes sont paramétrés pour
s’exécuter sur les coeurs CPU 0,1,18 et 19 tandis que les jobs Slurm s’exécutent
sur les coeurs CPU restants (2-17 et 20-35).
Vous pouvez donc, sans préparation supplémentaire, surveiller l’activité système des coeurs de benchmark quand vous lancez un job Slurm, comme ceci :
# Dans un shell
perf trace -C 2-17,20-35
# Dans un autre shell
srun true
Il y a cependant deux limites importantes à cette approche :
- Vous voyez également l’activité système associée aux jobs Slurm de tous les
autres utilisateurs. Si vous êtes malchanceux, quelqu’un d’autre va faire un
srun
au même moment et compliquer grandement l’interprétation de votre mesure. - Vous prenez du temps CPU sur une session interactive pour faire fonctionner
perf trace
, alors que les ressources CPU associées aux sessions interactives sont très limitées.
Je vous recommande donc de la réserver aux situations où l’approche précédente est inapplicable.
Approche par filtrage de l’activité associée à perf
Une dernière manière d’éviter de suivre les appels systèmes associés à
l’exécution de perf trace
avec perf trace
est de demander à celui-ci de les
ignorer avec l’option --filter-pids
, qui prend en paramètre une liste de PIDs
(identifiants de processus) séparés par des virgules et fait en sorte que ni
l’activité de ces processus, ni celle de perf
ne sera présente dans la sortie
de perf trace
.
Pour alléchante que soit cette dernière approche, elle a plusieurs problèmes :
- Dès que la sortie de
perf trace
suit un trajet compliqué à travers le système (comme c’est le cas lorsqu’on la redirige ou quand on lanceperf trace
viasrun
), le nombre de processus à exclure augmente, et la représentativité des mesures diminue d’autant. - Les PIDs des processus à exclure ne sont pas forcément connus d’avance, et les déterminer peut nécessiter des acrobaties shell complexes.
Tracepoints
A la fin de la partie sur perf list
, nous avons évoqué comment perf stat
nous permettait d’aller au-delà des appels système et de compter divers
événements internes au noyau Linux, via des tracepoints (points de code
instrumentés) que nous pouvons énumérer avec perf list tracepoint
.
Avec perf trace
, nous avons accès à des informations supplémentaires sur ces
tracepoints :
- Nous pouvons observer le déroulé temporel des passages à ces endroits du noyau
- Nous pouvons y connaître certaines informations choisies par l’auteur du tracepoint
Considérons, par exemple, la commande dd
suivante, qui écrit quelques
kilo-octets de données aléatoires dans un fichier du disque dur SATA du serveur
d’une façon fabuleusement inefficace :
dd if=/dev/urandom of=/hdd/${USER}/randomness.bin bs=42 count=123 oflag=sync
En examinant les appels système qu’elle fait, on n’apprendrait pas grand-chose :
srun --pty \
perf trace -s \
dd if=/dev/urandom of=/hdd/${USER}/randomness.bin bs=42 count=123 oflag=sync
En revanche, on peut en apprendre davantage en étudiant l’activité des tracepoints associés à différentes parties du noyau Linux impliquées dans l’écriture de données sur le disque :
- Le système de fichier utilisé sur
/hdd
(ext4, comme vous le diramount
) - Le périphérique bloc sous-jacent (
/dev/sda1
pour les intimes) - Le cache disque qui s’intercale grosso modo entre les deux
Utiliser perf stat
avec ces tracepoints nous permet déjà de formuler
quelques hypothèses :
srun --pty \
perf stat -e 'block:*,ext4:*,writeback:*' \
dd if=/dev/urandom of=/hdd/${USER}/randomness.bin bs=42 count=123 oflag=sync
Sortie de perf stat
On constate ainsi que certaines parties du noyau semblent traversées une fois par bloc, d’autres plusieurs fois par bloc (le nombre de fois où elles sont traversées est approximativement multiple du nombre de blocs), et d’autres plus rarement, ce qui suggère qu’elle sont appelées une fois tous les quelques blocs ou bien durant les phases d’initialisation et de finalisation du programme.
Et, bien sûr, on voit aussi que certaines parties du noyau ne sont pas traversées, ce qui peut dans certain cas être une information tout aussi importante.
Mais le niveau de détail accessible via perf trace
n’est tout simplement pas
comparable :
srun --pty \
perf trace -e 'block:*,ext4:*,writeback:*' \
dd if=/dev/urandom of=/hdd/${USER}/randomness.bin bs=42 count=123 oflag=sync
Sortie de perf trace
Avec ce nouvel outil, on commence vraiment à voir assez précisément qu’est-ce que fait le noyau Linux des requêtes d’écriture disque qu’on lui envoie, quelles parties du noyau sont sollicitées, quels ordres sont envoyés au matériel sous-jacent… et ce niveau de détail peut faire la différence quand on essaie de démêler pourquoi une application relativement complexe est loin d’atteindre les performances d’E/S théoriquement permises par le périphérique de stockage qu’elle utilise.
Exercice : Répétez cette analyse en remplaçant oflag=sync
par
conv=fsync
, puis en supprimant carrément cette option. Qu’est-ce que ça
change au niveau de l’activité noyau ?
Graphes d’appels
Motivation
Parfois, il arrive qu’on observe une activité système dont on ne sait pas vraiment d’où elle sort. Dans ce cas, il est utile de savoir quelle partie du programme appelant a causé cette activité système.
Par exemple, sur le résumé des appels système de la commande dd
étudiée
précédemment…
…on pourrait être surpris de voir 17 occurences de l’appel système fstat()
,
qui sert à obtenir des informations sur un fichier. Après tout, l’outil dd
n’a
qu’un fichier en entrée et un fichier en sortie, donc on voit mal pourquoi il va
se poser des questions sur au moins 15 autres fichiers.
Connaître les paramètres et le résultat de l’appel système n’aide pas trop ici :
srun --pty \
perf trace -e 'fstat' \
dd if=/dev/urandom of=/hdd/${USER}/randomness.bin bs=42 count=123 oflag=sync
En revanche, connaître par quel chemin dans le programme (pile d’appels) on en est arrivé à effectuer cet appel système aide beaucoup plus…
srun --pty \
perf trace -e 'fstat' --call-graph=dwarf \
dd if=/dev/urandom of=/hdd/${USER}/randomness.bin bs=42 count=123 oflag=sync
Sortie de perf trace
…et c’est cela que perf
appelle le graphe d’appels (call graph) : un graphe
qui, pour une fonction donnée, indique par quelles autres fonctions elle est
appelée et quelles autres fonctions elle appelle, dans un contexte donné (ici
les appels système fstat
). On parle de graphe d’appel et pas d’arbre d’appel
parce qu’une fonction peut s’appeler elle-même récursivement.
Avec cette information supplémentaire, nous apprenons ici que les deux premiers
appels à fstat
ont été effectués pendant le chargement du programme et de ses
dépendances par le dynamic loader et que les 15 appels suivants ont été fait
dans le cadre de la configuration de la locale de la glibc
, c’est à dire le
support d’un affichage dans plusieurs langues (ce qui implique d’afficher les
nombres décimaux de format différent par exemple).
Mais revenons un instant sur l’option --call-graph=dwarf
que nous avons
utilisée pour obtenir cette information. Comme vous pouvez vous en douter à sa
syntaxe, perf
supporte plusieurs techniques différentes pour reconstruire le
graphe d’appels, chacune avec ses avantages et inconvénients. Et le sujet est
assez complexe et important pour mériter une parenthèse théorique.
Techniques de reconstruction
Frame pointers (--call-graph=fp
)
Les frame pointers étaient historiquement utilisés par la plupart des outils
de déboguage, profilage et analyse dynamique. L’idée générale de cette approche,
c’est que dans une convention d’appel de fonction typique, à chaque fois qu’un
programme appelle une fonction, il pousse sur la pile des informations
permettant de remonter à la fonction appelante, puis à l’appelant de la fonction
appelante, et ainsi de suite. Et à chaque instant, un registre CPU (ebp
/rbp
sous x86) permet aux outils d’analyse de remonter cette liste chaînée.
Le problème, c’est qu’avec le temps, les compilateurs ont trouvé des techniques
pour gérer la pile d’appels sans utiliser un registre CPU pour ça. Et comme les
registres CPU sont une ressource très précieuse, ils ont commencé à se servir du
registre ebp
pour toutes sortes d’autres choses. Cette approche de
reconstruction de pile d’appels ne fonctionne donc plus sur les programmes
compilés avec les versions actuelles de GCC et clang, du moins avec leurs
réglages par défaut. Pour pouvoir l’utiliser, il faut recompiler toute la
portion de sa pile logicielle qu’on cherche à analyser avec l’option
-fno-omit-frame-pointer
, ce qui n’est généralement pas acceptable.
Last Branch Record (--call-graph=lbr
)
Une deuxième technique consiste à utiliser le Last Branch Record (LBR) des
processeurs Intel. Il s’agit d’une fonctionnalité d’analyse de performances du
CPU qui mémorise les derniers sauts effectués par le programme. Depuis la
génération Haswell (2013), on peut la configurer pour suivre chaque appel aux
instructions CALL
et RET
, qui sont typiquement utilisées par les compilateur
pour implémenter les appels de fonction. Le CPU maintiendra en fonction une pile
d’appel dans une petite banque de registres, que perf
lira de temps en temps.
Le problème, c’est que cette méthode ne fonctionne…
- Que sur certains matériels (CPUs Intel de génération >= Haswell).
- Que jusqu’à une certaine longueur de pile d’appels (32 sur
srv-calcul-ambulant
, 16 pour les générations avant Skylake), au-delà de laquelle la pile d’appel mesurée sera tronquée. Cette limite suffit pour de petits programmes, mais est insuffisante pour analyser un programme qui utilise beaucoup la récursion comme l’interpréteur CPython. - Que sur des programmes qui ont une configuration d’appel de fonction
“classique” (pas de
catch
d’exceptions C++, pas de coroutines, et si le compilateur utilise des implémentations alternatives d’appel de fonction comme l’inlining ou la récursion terminale, le LBR ne verra pas ces appels et doncperf
non plus…).
De plus, pour des raisons mystérieuses, il n’est actuellement (Linux 6.0) pas
possible d’utiliser cette méthode quand on surveille des tracepoints, comme le
fait perf trace
. Elle n’est compatible qu’avec les événements PMU
(Performance Monitoring Unit du CPU). Une discussion sur la mailing list de
perf suggère que c’est une limitation au niveau du noyau Linux qui pourrait être
levée par une version future de perf et du noyau.
Informations de déboguage (--call-graph=dwarf
)
En raison des inconvénients des approches précédentes, on doit souvent avoir recours à une troisième technique, où on enregistre les derniers kilo-octets de la pile du programme, puis les analyse en différé avec des informations de déboguage. C’est la même technique qu’utilisent les débogueurs pour afficher des piles d’appel.
Le nom “dwarf” de cette méthode est une référence au format DWARF utilisé pour stocker les informations de déboguage dans les binaires ELF utilisés par Linux.
Même si c’est souvent la seule disponible, cette méthode n’est pas non plus sans inconvénients :
- Elle suppose que les derniers kilo-octets de la pile du programme suffisent à
reconstruire la pile d’appels. Ce n’est pas vrai si le programme alloue des
grosses variables sur la pile ou a des piles d’appel profondes. On peut
augmenter la taille de la région de pile copiée, par exemple à 30 Ko avec
la syntaxe
--call-graph=dwarf,30000
, maisperf
refusera au-delà de 64 Ko. - Copier des kilo-octets de pile à chaque fois qu’un événement survient a un
coût en bande passante mémoire qui n’est pas négligeable, et analyser cette
information à posteriori consomme une grande quantité de temps CPU. Dans le
contexte de
perf trace
, on devra donc se restreindre à étudier des événements peu fréquents avec cette méthode. - Pour effectuer l’analyse finale, il faut disposer des informations de
déboguage de tous les programmes et bibliothèques utilisés pendant le job
à analyser (option
-g
de GCC et clang pour les programmes qu’on compile soi-même, paquets-debuginfo
sous openSUSE/Fedora/RHEL et-dbg
sous Debian/Ubuntu). - Et même quand l’ensemble de ces conditions sont remplies, la reconstruction de
piles d’appels ne va pas toujours jusqu’au bout, pour des raisons variées et
difficile à élucider (bogues de compilateur, de
binutils
, deperf
…).
Pour résumer, quand on veut des graphes d’appels avec perf
, on est généralement
avisé de commencer par l’option --call-graph=dwarf
, aggrémentée d’une taille
de copie plus grosse que la valeur par défaut si nécessaire pour de bons
résultats, et de prendre l’habitude d’installer les symboles de déboguage pour
les logiciels/bibliothèques dont on peut être amené à analyser la performance
sur ses machines de développement, ainsi que leurs dépendances.
A l’avenir, cette tâche rébarbative devrait être simplifiée par la maturation de
l’écosystème debuginfod, qui
vise à permettre une récupération automatique des symboles de déboguage par
perf
(entre autres) pour les paquets de toutes les distributions Linux
courantes.
Autres options
Similarités et différences avec perf stat
Comme d’autres commandes perf
que nous avons vu précédemment, perf trace
supporte les options --delay
/-D
, --cpu
/-C
, --event
/-e
ainsi que la
surveillance d’un processus, thread ou utilisateur système spécifique, avec des
sémantiques similaires. Comme nous avons déjà abordé ces options dans
la partie sur perf stat
, nous ne reviendrons pas
dessus ici.
Contrairement aux commandes perf
que nous avons vu précédemment, l’option
--event
/-e
de perf trace
supporte des motifs négatifs tels que
-e '!futex,!clock_nanosleep'
. Ceux-ci permettent de suivre tous les appels
système à l’exception de ceux qu’on a listés.
Options de filtrage
De cette partie du TP, vous avez probablement compris le principal défi
auquel on fait face quand on utilise perf trace
: survivre à l’énorme flux
d’information qui en sort.
Sans surprise, cette commande possède donc une belle palette d’options pour sélectionner des événements particulièrement intéressants et n’afficher qu’eux. Quelques exemples :
--duration N.M
où N.M est un nombre de millisecondes, permet de n’afficher que les appels systèmes qui durent suffisamment longtemps. Cette option n’est pas utilisable quand on étudie des tracepoints, puisque ceux-ci ne sont que des points temporels sans notion de début et de fin.--failure
permet de n’afficher que les appels système qui ont échoué.--max-events=N
permet d’arrêter l’enregistrement après N événements.--switch-on <event>
et--switch-off <event>
permettent d’attendre qu’un certain événement soit survenu avant de démarrer/arrêter l’enregistrement.--filter
permet de n’afficher que des événements dont les paramètres vérifient certains prédicats logiques.
Clarifions un peu cette dernière option en prenant l’exemple d’un job
perf trace
qui fait un suivi des interruptions matérielles :
srun --pty \
perf trace -a -e irq:irq_handler_entry \
sleep 0.01
Sortie de perf trace
Comme vous pouvez le constater, même sur un enregistrement de 10 millisecondes
effectué sur un système au repos, la sortie est monopolisée par la gestion du
trafic réseau (IRQ 72), interruption très fréquente. Nous pouvons choisir
d’ignorer cette interruption matérielle avec le filtre --filter 'irq != 72'
,
et dans ce cas même un enregistrement de quelques secondes devient lisible.
srun --pty \
perf trace -a -e irq:irq_handler_entry --filter 'irq != 72' \
sleep 10
Sortie de perf trace
Vous trouverez plus de détail sur la syntaxe utilisée par --filter
dans
la section “Event filtering” de la documentation des trace events du noyau.
Suivis des défauts de page
Une option particulièrement intéressante de perf trace
est l’option
--pf
/-F
, qui permet d’analyser les défauts de page. Sous Linux, cette
interruption matérielle qui survient lorsqu’on tente d’accéder à une adresse
mémoire qui n’est pas associée à de la RAM physique est utilisée pour
toutes sortes de choses (swapping, allocation paresseuse de mémoire avec
gestion du NUMA par first touch), et ces astuces d’implémentation peuvent
parfois causer des problèmes de performance difficiles à comprendre. Il est donc
intéressant d’avoir un outil pour en faire le suivi.
Enregistrement des données
Il est aussi possible d’enregistrer les informations produites par perf trace
avec la commande perf trace record
puis de les analyser en différé avec
perf trace -i <fichier perf.data>
. Cela génère du trafic vers le support de
stockage sous-jacent, donc
les précautions discutées précédemment
demeurent valables.
perf report
Les outils que nous avons vus jusqu’à présent fournissent soit un comptage
d’événements sur une certaine fenêtre de temps (perf stat
), soit une
information à grain fin (perf annotate
, perf trace
). Ils peuvent donc être
difficiles à utiliser sur des programmes complexes, où l’on ne veut pas
seulement connaître la nature de l’activité système mais aussi localiser son
origine (code source de l’application, bibliothèque utilisée…).
Pour répondre à ce besoin, on peut utiliser perf report
, qui s’utilise en
association avec perf record
(comme perf annotate
) et détermine quelles
fonctions d’une application et de ses dépendances génèrent l’essentiel de
l’activité système à laquelle on s’intéresse. C’est le type d’analyse que l’on a
le plus souvent en tête quand on parle de “profilage”.
Prérequis
Pour corréler des mesures à des emplacements dans le code source de
l’application, perf report
a besoin de comprendre la façon dont l’application
s’exécute. En simplifiant un peu…
- L’analyse sera relativement simple pour les programmes compilés avant
l’exécution, le cas habituel en C/++, Fortran et Rust. Elle est basée sur les
métadonnées des binaires ELF, communes à tous ces langages. Il faudra juste
penser à compiler avec des informations de déboguage (flag
-g
de GCC et clang) pour des résultats optimaux. - Quand les programmes sont compilés à la volée, le cas habituel en Java,
JavaScript et Julia, il faudra que
perf
puisse communiquer avec le compilateur pour obtenir les informatiques requises. Beaucoup de compilateurs populaires le permettent, mais souvent au prix d’une reconfiguration/recompilation avec des options spéciales qui ne sont pas activées dans les paquets binaires de la plupart des distributions Linux… - Et quand les programmes sont interprétés, le cas habituel en Python, Ruby et bash, on ne pourra souvent se rapporter qu’au code source de l’implémentation de l’interpréteur et des bibliothèques externes compilées statiquement, ce qui sera rarement satisfaisant.
Notez la répétition de l’expression “cas habituel” ci-dessus. Le modèle
d’exécution n’est pas une propriété d’un langage, mais d’une
implémentation de ce langage. On peut compiler du C++ à la volée comme le fait
l’interpréteur cling
de ROOT, tout comme on peut compiler du Python
statiquement (Cython, Pythran) ou à la volée (Numba, PyPy). Mais la plupart des
langages de programmation ont un type d’implémentation privilégié.
Par ailleurs, nous verrons plus loin que comme perf annotate
et perf trace
,
perf report
pourra effectuer une analyse plus fine si il dispose des
informations de déboguage de l’application et de tous les binaires externes
qu’elle utilise : noyau du système d’exploitation, bibliothèques…
Profil simple
Dans cette partie, nous allons nous intéresser à un programme qui implémente une simulation physique simple : un gaz d’électrons, protons et neutrons dans une boîte avec des interactions électrostatiques et nucléaires entre ces particules. Le programme est écrit dans un style qui rend hommage aux pratiques modernes de programmation en physique des particules, avec notamment un important recours aux techniques de programmation orientée objet.
Comme les programmes précédents, ce programme prend deux paramètres :
- Un nombre de particules à simuler
- Un nombre de pas de temps à simuler
Si vous ajustez ces paramètres, gardez en tête que la complexité du calcul est quadratique en fonction du nombre de particules puisque chaque particule interagit avec toutes les autres.
Un premier examen de l’exécution via perf stat
suggère que le CPU ne rencontre
pas de difficulté majeure pour exécuter ce programme…
srun --pty \
perf stat -d \
./TPhysics.bin 500 100
…mais comme nous l’avons vu précédemment, ce n’est pas une condition suffisante pour conclure que le programme fait un usage optimal du temps CPU.
Voyons maintenant l’éclairage nouveau apporté sur cette question par
perf report
. Comme avec perf annotate
, on doit d’abord enregistrer des
données avec perf record
, puis visualiser les mesures dans un deuxième temps
avec perf report
:
srun --pty \
perf record \
./TPhysics.bin 500 100 \
&& perf report
Nous nous retrouvons dans une interface console interactive ayant certaines
similarités avec celle de perf annotate
. Après un bref en-tête donnant des
statistiques globales sur la mesure (nombre d’échantillons enregistrés et nombre
approximatif de cycles CPU écoulés) et des titres de colonnes, perf report
affiche une table qui indique, par ordre décroissant de fréquence, les fonctions
qu’exécutait le plus fréquemment le programme lorsque des échantillons ont été
enregistrés.
Pour chaque fonction, perf report
indique…
- Le pourcentage des échantillons où le programme se trouvait dans une fonction
donnée, qui est un estimateur du pourcentage du temps CPU passé à exécuter
chaque fonction.
- Un code couleur est utilisé pour mettre en valeur les fonctions qui semblent contribuer grandement à la consommation de CPU.
- La commande exécutée, information qui peut être utile quand on étudie des
activités multi-processus telles qu’un pipeline
bash
. - Le binaire d’où est issu la fonction exécutée (programme ou bibliothèque).
- Le nom de fonction/symbole ELF, avec un petit indicateur
[.]
qui indique que les fonctions qu’on regarde proviennent de code applicatif. Pour du code noyau, ce serait[k]
.
…et l’on constate donc que notre programme emploie l’essentiel de son temps CPU à exécuter différentes parties de l’allocateur mémoire de la bibliothèque standard C. Il n’est donc pas limité par le calcul flottant, comme on aurait pu le penser naïvement.
On observe aussi une première difficulté liée à l’utilisation de perf report
:
de par son mode de fonctionnement, cet outil marque des nuances entre des points
d’implémentation qui n’ont pas d’importance pour nous. Par exemple, au premier
niveau d’analyse que nous souhaitons avoir ici, la nuance entre _int_free
,
les différentes overloads de operator delete
et operator delete@plt
n’est
pas pertinente, et on voudrait avoir juste une somme des coûts CPU associés à
l’appel de de l’allocateur mémoire. Nous verrons plus loin comment l’obtenir.
Enfin, mentionnons que l’on peut sélectionner l’une des fonctions du programme
avec les flèches haut/bas du clavier et afficher son assembleur annoté dans le
style de perf annotate
avec la touche a
du clavier. Comme dans
perf annotate
, la touche h
permet d’afficher l’aide des commandes clavier
et la touche q
permet de quitter perf report
.
Exercice : Afficher l’assembleur annoté de TParticle::reidInteraction
et
découvrez de quelle nature sont ses instructions CPU les plus chaudes.
Référez-vous à la partie sur perf annotate
si vous avez oublié le mode de fonctionnement de l’assembleur annoté.
Critères de tri
Fondamentalement, perf report
produit un histogramme : on groupe les
échantillons d’activité système observée selon certains critères, pour
l’activité CPU c’est par défaut la commande (comm
), le binaire (dso
) et la
fonction (sym
) où une activité a été observée. Puis on indique dans quels cas,
selon ces critères, on observe le plus d’activité système.
Il est possible, si on le souhaite, de modifier ces critères de tri. Par exemple, on peut demander un histogramme à gros grain, qui ne trie que par commande et par binaire…
perf report --sort comm,dso
…et on en déduit qu’au plus 39% du temps CPU écoulé est attribuable au code
directement écrit dans TPhysics.cpp
et TPhysics.hpp
, le reste étant passé
dans du code de la bibliothèque standard. Autrement dit, si l’on ne modifiait
que le code de la simulation sans affecter le nombre et le type d’appels à la
bibliothèque standard, le programme irait au mieux 39% plus vite.
(“Au mieux” parce qu’une partie du code de la bibliothèque standard se retrouve
inlinée dans TPhysics.bin
et ne serait pas affectée par ces manipulations,
et aussi parce qu’il faudrait pour atteindre ce gain maximal ramener le temps
d’exécution du code métier à zéro.)
A l’inverse, on peut demander un histogramme à grain plus fin, par exemple en demandant pour quelles lignes de code source la plus forte activité système est observée :
perf report --sort comm,dso,sym,srcline
Ce ne sont que des exemples, bien d’autres critères sont disponibles, notamment
le PID (utile pour discriminer le processus source dans des environnements
multi-processus) et le TID (pour discriminer le thread source dans des
programmes multi-thread). Consultez la partie sur l’option --sort
de
perf help report
pour en savoir plus.
Exercice : Utiliser perf report
pour afficher un histogramme où les
échantillons sont groupés par binaire et par ligne de code source uniquement.
Vous observerez quelques erreurs de BFD (la bibliothèque issue de binutils
utilisée par perf
pour manipuler des binaires ELF), qui sont liées au fait que
l’interprétation d’informations de déboguage n’est malheureusement pas une
science exacte, surtout au niveau du code noyau.
Dans le cas présent, vous pouvez éviter ces erreurs en ne mesurant que
l’activité applicative, sans regarder celle du noyau. Il suffit pour cela de
relancer la commande perf record
en y ajoutant l’option -e cycles:uP
(relisez la partie sur perf list
si vous avez
oublié ce que ce :uP
signifie).
Graphes d’appels
Motivation
Dans la partie sur perf trace
, nous avons vu que la mesure de graphes
d’appels nous permet de comprendre l’origine d’une activité
système. Dans le cadre de perf report
, cette
mesure est aussi possible, et même très utile :
- Elle nous permet de discriminer des appels à une fonction qui surviennent pour différentes raisons. Dans notre exemple, nous pouvons ainsi savoir quelles parties du code passent le plus de temps à appeler l’allocateur mémoire, afin de prioriser les optimisations associées.
- Elle nous permet d’acquérir une vision plus haut niveau de l’activité système de notre programme. Dans notre exemple, nous n’avons plus besoin de nous préoccuper de détails d’implémentation de l’allocateur mémoire pour effectuer notre analyse.
Cependant, il y a aussi quelques nouvelles précautions à prendre par rapport à
perf trace
, qui sont principalement liées au fait que…
perf record
est généralement utilisée pour échantillonner à haute fréquence (ex: par défaut, on échantillonne le pointeur d’instruction à plusieurs kHz), là oùperf trace
est principalement pensée pour suivre des appels système relativement peu fréquents.perf record
produit un fichierperf.data
en sortie au lieu de traiter les données à la volée, ce qui peut créer un goulot d’étranglement en termes de bande passante de stockage ou bien perturber l’exécution d’un benchmark sensible à la performance de stockage.
Pour éviter ces problèmes, on voudra généralement spécifier manuellement une fréquence d’échantillonnage peu élevée et parfois écrire les données de sortie en RAM.
Exemple DWARF
Voici un premier exemple de cycle perf record
/report
qui prend ces deux
précautions, en utilisant des graphes d’appels basées sur les informations de
déboguage
(qui constituent, pour rappel, le meilleur choix par défaut quand on est
indécis) :
srun --pty \
perf record -F 1000 --call-graph=dwarf -o /dev/shm/${USER}/perf.data \
./TPhysics.bin 500 100 \
&& mv /dev/shm/${USER}/perf.data . \
&& perf report
On peut constater la présence de nombreuses nouveautés dans l’affichage :
- L’ancienne colonne
Overhead
est renommée enSelf
et placée en 2e position. - Il y a une nouvelle colonne
Children
, qui représente une estimation du temps passé dans une fonction X ainsi que dans l’ensemble des fonctions directement ou indirectement appelées par X. Cette colonne est la nouvelle clé de tri. - Au niveau des symboles, le code de discrimination entre fonctions utilisateur
et noyau cette version de
perf
semble un peu bogué… perf
est parfois (hélas pas à tous les coups) en mesure d’estimer le temps CPU consommé par les fonctions inlinées, ce qui est utile car elles sont souvent critiques pour les performances.
Une lettre +
a également fait son apparition sur la gauche de l’affichage. Ce
signe nous indique qu’en mettant en surbrillance une fonction et en appuyant sur
la touche Entrée, on peut afficher la répartition du temps CPU entre les
fonctions appelées, et ce récursivement si on le souhaite :
Limites du DWARF
Voilà pour les bonnes nouvelles. Maintenant, la grosse mauvaise nouvelle. Il ne
vous aura pas échappé que seulement 79% des piles d’appels ont correctement été
reconstruites jusqu’à la fonction main
. Ceci est une occasion de plus de
répéter un point central dans l’interprétation de toutes les mesures perf
basées sur l’analyse des informations de déboguage :
Les analyses basées sur les informations de déboguage sont loin d’être infaillibles, et ce pour toutes sortes de raisons difficiles à analyser pour le non-expert : bogues du compilateur, de
binutils
, deperf
, ou taille d’échantillon de pile insuffisante.
En pratique, pour les 21% de piles d’appels restantes, la remontée de la pile
d’appels a bien fonctionné jusqu’à un certain point, puis une erreur s’est
produite en cours de route et perf
a atterri à une adresse d’instruction
insensée avant d’abandonner, comme on peut s’en convaincre avec une analyse à
gros grain de ces échantillons pathologiques basée sur le binaire source :
perf report --call-graph=comm,dso
La conséquence de cela, c’est que les pourcentages Children
obtenus sur un
graphe d’appels DWARF ne sont pas fiables à 100%. Au mieux, les échantillons
mal reconstruits sont équirépartis dans le programme et on a “juste” besoin de
normaliser les valeurs obtenues dans le graphe d’appels qui commence à main
par 1/79% = 1.26. Au pire on a un problème qui se produit toujours au même
endroit du code, et cela créera un biais systématique d’au plus 21% dans les
mesures.
Sans information complémentaire, une façon prudente d’interpréter les données
est de conclure quel les pourcentages “Children” que l’on obtiendrait avec une
mesure parfaite sont compris entre la valeur mesurée X et X+(100-M) où M est
le pourcentage “Children” mesuré pour la fonction main
(qui devrait être ~100%
pour tout programme dont le temps d’exécution n’est pas négligeable, en
l’absence d’erreur de reconstruction des piles d’appels).
Pour faire mieux, il faut avoir sous la main une deuxième technique de
profilage qui n’est pas sujette aux mêmes biais que les graphes d’appels DWARF
afin d’évaluer le niveau de biais lié aux piles d’appels tronquées. Cela peut
être un profilage manuel au sein de l’application, mais dans le cadre de
perf report
, on peut aussi parfois utiliser les
graphes d’appels LBR.
Exemple LBR
Au niveau de perf record
, on utilise ce style de graphe d’appels avec l’option
--call-graph=lbr
:
srun --pty \
perf record --call-graph=lbr \
./TPhysics.bin 500 100 \
&& perf report
Sur ce programme qui reste relativement simple, et donc sous la limite des 32 appels de fonction imbriqués supportés par le LBR de notre CPU, le graphe d’appels par LBR produit de bons résultats. On peut observer les différences suivantes avec le graphe d’appels par information de déboguage :
- Presque toutes les piles d’appels sont ici reconstruites jusqu’à
main
. - La discrimination entre code applicatif et code du noyau Linux fonctionne à tous les coups.
- On n’a plus de visibilité sur les fonctions inlinées.
- L’information LBR est beaucoup plus légère que les copies de piles requises
pour l’analyse DWARF, donc on n’a plus besoin d’imposer une limite de
fréquence d’échantillonnage ou d’émettre le fichier de sortie sur
/dev/shm
. L’analyse parperf report
est aussi plus rapide.
Au niveau du résultat, on peut comparer le premier niveau de hiérarchie sous
main
pour les deux techniques, en descendant jusqu’à la première fonction non
inlinée dans le cas du graphe d’appels DWARF pour faciliter la comparaison :
Une petite règle de trois permet de se convaincre qu’il n’y a pas de biais
systématique majeur ici : en renormalisant les pourcentages “Children” du graphe
d’appels DWARF par 1/0.7978 (l’inverse du pourcentage d’échantillons correctement
reconstruit jusqu’à main
), on trouve…
- 40.6% pour
TProton::interaction
- 19.3% pour
TNucleon::interaction
- 14.1% pour
TChargedParticle::interaction
- 8.5% pour
TVector::vectorOp
- 1.8% pour
std::vector<double>::operator=
…ce qui est en plutôt bon accord avec le profil mesuré par la méthode LBR. On
notera cependant quelques différences. Par exemple l’approche DWARF attribue le
coût de déallocation des std::vector
à la fonction _int_free
de la glibc
alors que l’approche LBR l’attribue à l’opérateur delete
de la libstdc++
, et
il semblerait que l’approche DWARF ne parvienne pas à reconstruire les piles
d’appels passant par la méthode virtuelle TChargedParticle::interaction
.
Exercice : Répétez cette analyse avec les enfants de la fonction
TParticle::reidInteraction
.
Notez la présence d’une indication _start
dans la visualisation de
perf report
, qui représente la pile d’appels au-dessus de la fonction
courante (de _start
jusqu’à la fonction active).
Contourner la limite de taille du LBR
Sur des programmes plus complexes, l’approche LBR n’est souvent pas directement applicable, car les piles d’appels sont profondes et dépassent largement la limite de 32 appels de fonction du matériel. C’est particulièrement vrai quand on profile un framework scripté avec du Python, car l’implémentation de l’interpréteur CPython utilise beaucoup d’appels récursifs.
Dans ce genre de cas, on peut demander à perf report
de tenter de reconstruire
les piles d’appels tronquées via une technique astucieuse, le LBR-stitching.
L’idée est la suivante : considérons un CPU avec une capacité LBR de 4 appels et
supposons que l’on ait un programme effectuant la série d’appels imbriqués
suivante :
- A1 appelle A2…
- …qui appelle A3
- …qui appelle A4
- …qui appelle A5
- …qui appelle A6
- …qui appelle A5
- …qui appelle A4
- …qui appelle A3
En supposant qu’un temps CPU significatif soit écoulé dans toutes les fonctions,
perf
observera dans ses échantillons les piles d’appels suivantes:
- A1
- A1 → A2
- A1 → A2 → A3
- A1 → A2 → A3 → A4
- A2 → A3 → A4 → A5 (tronqué par le LBR)
- A3 → A4 → A5 → A6 (tronqué par le LBR)
En corrélant les piles d’appels observées dans différents échantillons, perf
pourra alors reconstruire les piles d’appels tronquées en déduisant que A2
a probablement été appelée par A1 et A3 a probablement été appelée par A2.
L’opération est moins hasardeuse que cette description simplifiée ne pourrait
le laisser paraître, car perf
n’a pas seulement accès au nom de la fonction
appelante, mais plus précisément à l’adresse de l’instruction CALL
associée.
Mais cela reste un post-traitement dont la fiabilité n’est pas parfaite :
- En cas d’appel à des méthodes virtuelles dans du code orienté objet, une même
instruction
CALL
peut appeler plusieurs fonctions différentes. - Si beaucoup de fonctions ne font rien d’autre qu’appeler d’autres fonctions
(cas de codes appliquant les bonnes pratiques d’ingénierie logicielle de façon
un peu trop orthodoxe),
perf
aura peu d’échantillons à ces endroits, ce qui limitera l’applicabilité du stitching. - Plus la fréquence d’échantillonnage est faible, moins cette approche fonctionne.
- Ce post-traitement est plus vulnérable que l’approche LBR pure au code “pathologique” (ex: lancement et capture d’exceptions).
On peut activer le stitching en passant l’option --stitch-lbr
à
perf report
. Et quand cela ne fonctionne pas, une alternative courante pour
valider les résultats des graphes d’appels DWARF est de profiler manuellement son
code, par exemple en minutant et affichant le temps écoulé pendant l’exécution
d’étapes de traitement de haut niveau (idéalement au moins 100ms).
Options d’affichage du graphe d’appels
perf report
dispose de plusieurs options pour configurer l’affichage des
graphes d’appels. On peut notamment mentionner…
- L’option
--call-graph
qui se compose de plusieurs sous-options séparées par des virgules (--call-graph=x,y,z,...
). Les quatre premières sont les plus importantes :- La sous-option
print_type
définit le comportement global quand on appuie sur Entrée après avoir mis une fonction en surbrillance. Les choix les plus utiles sont :fractal
: Les pourcentages de temps CPU ne sont plus absolus, mais relatifs à la fonction choisie. Soyez prudent avec ce mode, il est facile de s’y laisser piéger par un gros pourcentage relatif qui correspond en fait à un faible pourcentage absolu (qui peut ne même pas être statistiquement significatif).flat
: La vue n’est plus hiérarchique et on a directement une liste de toutes les piles d’appels associées à la fonction sélectionnée. Ce mode est utile pour des programmes avec des piles d’appels très profondes.graph
: Mode par défaut (vue hiérarchique avec des pourcentages absolus), utile quand on veut configurer les autres sous-options.
- La sous-option
threshold
, définit en dessous de quel pourcentage de temps CPU une fonction sera supprimée de l’affichage (0.5% par défaut). Cela permet d’obtenir un affichage plus ou moins détaillé. - La sous-option
print_limit
n’est considérée qu’en mode--stdio
et limite le nombre de piles d’appels enfant affichées par parent (0 = illimité est le choix par défaut) - La sous-option
order
permet de basculer entre un affichage basé sur les enfants et un affichage basé sur les parents :- Le mode par défaut est
caller
, où l’on affiche les fonctions appelées par la fonction sélectionnée (fonctions enfants, caller-based call graph). - En mode
callee
,perf
affiche quelles fonctions font le plus souvent appel à la fonction sélectionnée (fonctions parents, callee-based call graph). Ce mode est utile quand on veut savoir qui fait le plus souvent appel à une fonction utilitaire. Il est aussi plus proche du fonctionnement interne deperf report
(partir d’un échantillon CPU et remonter la pile d’appels), donc utile pour déboguer.
- Le mode par défaut est
- La sous-option
- L’option
--percent-limit
a un effet analogue à la sous-optionthreshold
de--call-graph
, mais s’applique aux entrées d’histogramme (la liste affichée lors de l’ouverture deperf report
), alors quethreshold
s’applique aux entrées parent/enfant affichées quand on parcourt la hiérarchie des appels avec la touche Entrée. - L’option
--max-stack
permet de contrôler la limite de taille de pile d’appels après laquelleperf report
abandonnera l’idée de remonter jusqu’au point d’entrée (127 par défaut).
Exercice : Affichez quelles parties du code font le plus appel à
_int_malloc
, free
et __dynamic_cast
, et dans quelles proportions.
Liste des binaires utilisées
Quand on utilise des graphes d’appels basés sur les informations de déboguage DWARF (ce qu’on est souvent amené à faire sur les gros programmes), on est fréquemment confronté au besoin d’installer des paquets de déboguage, et donc de savoir quels paquets il faut installer.
Dans cette optique, la commande perf buildid-list
peut être utilisée pour
savoir quels binaires ont été utilisés lors de l’exécution d’un programme et
quels sont les build-id associés :
perf buildid-list
Le build-id est une somme de contrôle d’un binaire qui permet de différencier différents binaires qui portent le même nom mais ne correspondent pas exactement à la même version du code source ou n’ont pas été compilés avec les mêmes options. Le gestionnaire de paquets de certaines distributions Linux, dont openSUSE, peut installer des symboles de déboguage sur la base d’un build-id sans avoir besoin de chercher le nom et la version du paquet associé.
Tracepoints
Comme on l’a vu dans la partie sur perf annotate
, perf record
évalue par
défaut quels régions du code consomment la plus grande quantité de cycles CPU,
mais il est possible de suivre d’autres événements avec la désormais habituelle
option --event
/-e
.
Par exemple, avec perf record -e L1-dcache-load-misses
, on peut déterminer
dans quelles régions du code il y a le plus de défauts de cache L1. Cela peut
offrir un éclairage alternatif à la seule étude de consommation de cycles CPU,
qui est utile quand on optimise l’utilisation de ressources CPU comme les caches
qui sont partagées entre différentes régions du code. En effet, quand ces
ressources sont sous tension, on peut observer des comportements
contre-intuitifs, par exemple ajouter un nouveau module de code peut ralentir un
module préexistant qui n’a pas été modifié.
Mais il y a un autre type d’événements qui est particulièrement intéressant à
analyser avec perf record
et perf report
, c’est les événements issus du
noyau Linux, comme les tracepoints.
En effet, nous avons vu que leur suivi “en direct” avec perf trace
peut être
parfois difficile :
- Il est facile de se retrouver submergé dans par un journal gigantesque d’événements, bien que ce journal soit souvent très redondant.
- La stratégie de
perf trace
d’afficher du texte à chaque fois que l’événement survient ne passe pas à l’échelle quand l’événement étudié est fréquent. - Si l’action d’afficher du texte déclenche l’événement étudié,
perf trace
peut se retrouver dans une situation indésirable de récursion infinie.
Nous allons voir maintenant que l’étude de ces mêmes événements avec
perf record
et perf report
fournit un éclairage différent qui peut aider à
surmonter ces difficultés.
Appels système d’une commande
Considérons la commande suivante, qui lit des zéros depuis /dev/zero
est les
rejette dans /dev/null
. C’est un benchmark simple de la performance des
pseudofichiers du noyau Linux.
srun --pty \
bash -c 'tail -c10G /dev/zero >/dev/null'
On l’a vu, perf trace
peut nous donner un premier aperçu des appels systèmes
effectués par cette commande…
srun --pty \
perf trace -s \
bash -c 'tail -c10G /dev/zero >/dev/null'
…et on constate sans surprise que l’essentiel de l’activité système générée et
du temps d’exécution associé sont liés aux lectures et écritures du système de
fichier effectuées par la commande tail
.
Mais il y a aussi des choses plus surprenantes, notamment le fait que le nombre
de lectures d’écritures n’est pas le même. Et on pourrait aussi se poser des
questions d’implémentation, par exemple vouloir savoir si la taille des lectures
et écritures en octets est toujours la même. Le mode “résumé” de perf trace
que nous venons d’utiliser ne permet pas de répondre à ces questions.
A raison de millions d’appels système read
et write
effectués, il ne serait
pas raisonnable d’afficher la liste complète de ces appels système avec
perf trace
et la relire manuellement. Il serait aussi laborieux d’extraire la
sortie de perf trace
dans un fichier et l’analyser avec un script, car elle
serait très volumineuse. C’est donc un cas où perf trace
n’est pas l’outil
adapté.
Voyons maintenant ce que perf record
et perf report
peuvent nous dire sur
les appels système read
et write
effectués par cette commande :
srun --pty \
perf record -e '{syscalls:sys_enter_read,\
syscalls:sys_exit_read,\
syscalls:sys_enter_write,\
syscalls:sys_exit_write}' \
-o /dev/shm/${USER}/perf.data \
bash -c 'tail -c10G /dev/zero >/dev/null' \
&& mv /dev/shm/${USER}/perf.data . \
&& perf report
Nous introduisons ici une nouvelle fonctionnalité de perf record
, les
group events, reconaissable à sa syntaxe {}
qui doit être mise dans une
chaîne de caractère pour ne pas être mal interprétée par bash
. Elle permet de
visualiser simultanément l’activité de plusieurs événements (ici l’entrée et
la sortie des appels à read
et write
) sous colonnes “Overhead” séparées dans
perf report
.
Par ailleurs, notez que le volume de données produit est si important que nous
avons besoin d’utiliser /dev/shm
pour stocker la sortie. Nous reviendrons sur
cette question.
Nous voyons que contrairement à perf trace
, perf report
ne nous donne pas
une liste ordonnée des appels système effectués, mais un histogramme de ceux-ci,
ce qui est bien plus exploitable pour cet exemple. Il en ressort que…
- Les lectures du fichier source s’effectuent toujours au sein du même tampon,
et
tail
demande toujours 8192 octets au noyau. - Le noyau fournit toujours l’intégralité des 8192 octets demandés par
read
. - Les écritures de fichier, elles, s’effectuent avec une granularité de 4096
octets et depuis deux tampons différents (l’un des deux étant le tampon
utilisé pour
read
avec un décalage de +4096 octets). C’est a priori étonnant et à examiner de plus près. - Le noyau écrit toujours l’intégralité des 4096 octets fournis à
write
.
En revanche, par rapport à perf trace
, nous avons perdu une information
importante, celle de l’ordre dans lequel se produisent les événements. C’est un
compromis inhérent à la représentation en histogramme de perf report
.
Nous pouvons obtenir une idée de cet ordre en étudiant un bref extrait de la
très longue sortie de perf trace
, après avoir indiqué à ce dernier d’attendre
100ms après le démarrage du programme pour ne pas voir la phase
d’initialisation :
srun --pty \
perf trace --delay 100 \
-e read,write \
bash -c 'tail -c10G /dev/zero >/dev/null' 2>&1 \
| head -n50
Et nous pourrions ensuite vouloir savoir si des chemins de code différents sont
à l’origine de ces deux types d’appels différents à l’appel système write
, mais
avant cela il va nous falloir aborder un prérequis à ce type d’analyse…
Améliorer l’efficacité
Si vous avez fait attention aux sorties des commandes perf record
précédentes,
vous avez peut-être tiqué sur les volumes de données produits. Il y a deux
raisons à cela :
- L’événement que nous suivons est très fréquent (plusieurs millions en quelques secondes).
- Par défaut,
perf record
essaye d’enregistrer chaque occurence des événements tracepoint.
Ce dernier comportement est cohérent avec celui de perf trace
, mais dans le
cas où nous sommes il est inadapté, car il nous fait enregistrer des millions
d’événements qui sont en réalité presque tous identiques. La situation
s’aggraverait encore si on essayait d’enregistrer des graphes d’appels via la
méthode DWARF, puisque cela ajoute un volume de données important par échantillon.
Nous avons déjà abordé ce problème précédemment, et évoqué une solution
simple : enregistrer un petit échantillon des événements étudiés, qui soit aussi
statistiquement représentatif que possible. Il suffit pour cela de spécifier une
fréquence d’échantillonnage avec l’option --freq
/-F
:
srun --pty \
perf record -F 10000 -e syscalls:sys_enter_write \
bash -c 'tail -c10G /dev/zero >/dev/null' \
&& perf report
Vous constaterez que du fait d’un léger biais statistique, la répartition des
échantillons n’est pas de 50%/50% entre les deux appels à write
. Mais le
niveau de biais observé reste suffisamment faible pour que la mesure soit
exploitable, et ce avec un fichier de données qui ne fait plus que 1 Mo.
En conclusion, les fréquences d’échantillonnage par défaut de perf record
ne
sont pas toujours adéquates (particulièrement sur les tracepoints), et il est
donc recommandé de toujours évaluer rapidement la fréquence de l’événement
étudié avec perf stat
ou perf trace -s
sur l’application cible et en déduire
une valeur judicieuse pour le paramètre -F
de perf report
. Au bout du
compte, la valeur optimale est celle…
- Qui produit une statistique suffisante.
- Qui génère un volume de données raisonnable.
- Qui perturbe aussi peu que possible l’exécution du code étudié.
Exercice : Utilisez les différentes fonctionnalités de perf report
abordées précédemment (notamment --call-graph
et --sort
) pour répondre
à la question soulevée ci-dessus : de quel code provient l’utilisation de deux
tampons différents pour les écritures ?
Notez que les graphes d’appels LBR ne sont pas supportés quand on enregistre des
événements tracepoint. Je ne comprends pas pourquoi (le message d’erreur n’est
pas cohérent avec ce que je comprends du mode de fonctionnement du LBR), mais je
peux vous dire que c’est toujours le cas dans les versions récentes de perf
(6.0.7), donc c’est une limitation avec laquelle il va sans doute falloir
composer dans votre pratique quotidienne de l’outil.
Rappelez-vous aussi que si vos graphes d’appels DWARF sont tronqués, vous pouvez
parfois régler le problème en augmentant la taille des échantillons de pile
enregistrés par perf
…
Autres options
perf record
et perf report
sont deux commandes qui jouent un rôle central
quand on utilise perf
pour analyser des grosses applications. Elles ont donc,
sans surprise, beaucoup d’options.
Options de perf record
Dans le cas de
perf record
, nous
avons déjà
abordé
de nombreuses
options
utiles,
mais quelques autres options méritent encore d’être mentionnées :
- Il existe un certain nombre d’options de performance qui permettent
de diminuer le nombre d’échantillons perdus quand on est forcé de travailler à
fréquence d’échantillonnage élevée, au prix d’une consommation de ressources
supplémentaire par
perf
: ordonnancement temps réel (--realtime
/-r
), taille des tampons de transit pour les données mesurées (--mmap-pages
/-m
), E/S asynchrone multi-thread (--aio
)… - On peut demander d’enregistrer divers détails pour chaque échantillon mesuré : addresses mémoire virtuelles et physiques, timestamps, numéro du CPU où l’échantillon a été mesuré, direction prise au niveau des instructions de branchement, contenu des registres CPU…
Filtrage non destructif
Au niveau de perf report
, il est possible de filtrer les données
affichées (par PID, TID, UID, cgroup, binaire/DSO, CPU…). La différence par
rapport aux options de filtrage de perf record
, c’est qu’ici les données ont
bien été enregistrées dans le fichier perf.data
, mais on choisit juste de ne
pas les afficher pour cette session perf report
.
Sélection temporelle
perf report
fournit également quelques options de filtrage et d’aggrégation
temporelle, qui sont très utiles quand on étudie une application qui alterne
entre plusieurs comportements dans le temps (typiquement des phases calcul vs ES
ou initialisation vs traitement vs finalisation) :
- Avec
--time
, on peut n’afficher que les données qui concernent une certaine fenêtre de temps dans les données issues du fichierperf.data
. La fenêtre peut être donnée en pourcentage (ex :--time 15%-90%
) ou en secondes (ex :--time 0.5,13.3
). Et il est possible de fusionner des données issues de plusieurs fenêtres de temps. - Avec
--sort time
, qui se combine avec les autre options--sort
abordées précédemment, on peut comparer l’activité de l’application au sein de “tranches de temps” de taille fixe contrôlées par l’option--time-quantum
. Cela est notamment utile pour repérer les changements qualitatifs de comportement de l’application et en déduire les bonnes valeurs à passer à--time
pour étudier ces comportements différents un par un.
Sortie non interactive
Une dernière option de visualisation utile de perf report
est --stdio
,
qui produit une version non interactive de la table qui peut facilement être
redirigée vers un fichier texte pour partage avec autrui ou post-traitement par
un script. Mais pour cette dernière tâche, il y a un outil perf
plus adapté
que nous aborderons ultérieurement.
perf top
Premier contact
Dans la partie sur perf report
, nous avons vu que
perf
permet d’étudier comment l’utilisation d’une ressources système, par
exemple sa consommation de temps CPU, se répartit entre les fonctions d’un
programme et des bibliothèques qu’il utilise.
perf top
étend ce suivi à l’ensemble des programmes en cours d’exécution sur
le système, fournissant en cela une alternative plus détaillée aux moniteurs
systèmes traditionnels :
srun --pty perf top
Sur cette capture d’écran, vous voyez la répartition du temps CPU entre les
différentes fonctions des applications en cours d’exécution (indicateur [.]
)
ou du noyau Linux (indicateur [k]
). Au moment où cette capture d’écran a été
prise, il n’y avait pas d’autre tâche que perf top
en cours d’exécution, par
conséquent nous voyons surtout la consommation CPU de perf
et l’activité noyau
associée.
Même si l’analogie avec les outils de monitoring système aide à se faire une
première idée, il y a quand même une nuance importante à retenir : contrairement
aux outils classiques, perf top
affiche par défaut un rapport sur l’ensemble
des événements survenus depuis son lancement. Donc si par exemple une tâche
qui ne consommait rien se met brusquement à consommer du CPU alors que
perf top
surveille le CPU depuis longtemps, la sortie de perf top
ne va
changer que progressivement, puisqu’au début cette nouvelle activité ne
représentera qu’une part négligeable du temps CPU utilisé depuis le lancement de
perf top
. En retour, l’intérêt de cette approche est que sur un système
“stationnaire”, la précision des statistiques s’améliorera au fil du temps.
Pour obtenir un comportement plus analogue à celui d’un moniteur système
classique, où l’on n’affiche que l’activité système survenue depuis le dernier
rafraîchissement de l’affichage, il faut utiliser l’option --zero
, qui
s’abbrévie en -z
et que l’on peut aussi activer avec le raccourci clavier Z
quand perf top
est déjà en cours d’exécution.
Dans une telle utilisation, on aura souvent intérêt à ajuster aussi le paramètre
--delay
, abbrévié en -d
, qui contrôle la fréquence de rafraîchissement de
l’affichage.
Précautions d’emploi
En s’exécutant, perf top
va faire l’équivalent d’un perf record -a
et d’un
perf report
à intervalles de temps régulier. Par conséquent…
- Pour des résultats optimaux,
perf top
doit disposer des symboles déboguages de l’ensemble des programmes et bibliothèques en cours d’utilisation. Selon la distribution Linux que vous utilisez, cela peut nécessiter une préparation système assez conséquente et laborieuse. Sursrv-calcul-ambulant
, cette préparation a été effectuée pour l’ensemble des applications et bibliothèques du système disposant de symboles de déboguage. - La durée de traitement du rapport “
perf report
” doit être inférieure au temps entre deux rafraîchissements, sinon des cycles de rafraîchissement seront ratés. Et comme ce traitement s’exécute en parallèle des applications étudiées, il peut affecter leurs performances d’une façon qui biaisera la mesure. Il faut donc se restreindre à des mesures relativement économes en ressources CPU, et en particulier être très prudent avec l’option--call-graph=dwarf
.
En dehors de ces points de vigilance, l’utilisation de perf top
est très
proche de celle de perf record
et perf report
. La quasi-totalité des
options de perf top
sont communes avec l’une ou l’autre de ces commandes, et
je vous invite donc à
vous référer
aux sections associées pour plus d’informations.
perf probe
Au cours des sections précédentes, nous avons vu que perf
n’est pas seulement
un outil d’analyse de l’utilisation du temps CPU, mais peut surveiller un très
grand nombre d’événements système, ce qui permet un suivi de l’activité du CPU
et du noyau Linux à grain fin.
Mais l’ensemble des indicateurs d’activité système que nous avons suivis jusqu’à présent étaient basés sur des tracepoints, une forme d’instrumentation mise en place manuellement par les développeurs du noyau Linux en des points stratégiques du code du noyau. Avec cette instrumentation manuelle, il ne nous est par définition pas possible d’instrumenter du code qui n’a pas été pensé pour être instrumenté, que ce soit dans le noyau ou en-dehors, à moins d’être prêt à modifier et recompiler le code en question.
Dans cette section, nous allons voir que les tracepoints ne sont pas la seule
forme d’instrumentation supportée par perf
, et qu’il est en réalité possible
d’instrumenter presque n’importe quel code pour qu’il soit observable par perf
,
que ce code soit situé dans le noyau Linux ou dans une application ou
bibliothèque…
Infrastructure
L’instrumentation de code est une technique puissante pour répondre à toutes
sortes de questions de performances. Dès lors qu’on chercher à connaître un
paramètre d’un programme qui n’est a priori défini qu’à l’exécution (par
exemple la taille typique de messages reçus via le réseau), la manière la plus
simple de répondre à la question est d’instrumenter le code concerné pour en
extraire cette information, fût-ce par un simple printf()
bien placé suivi
d’une recompilation.
Mais recompiler du code peut être laborieux, et faire des statistiques sur de
l’information issue d’un printf
dont le format est défini par l’utilisateur
peut l’être tout autant. C’est pourquoi Linux fournit un grand nombre d’outils
pour extraire de façon standardisée de l’information d’un code, avec ou sans
recompilation, et que le code ait été conçu pour ça à la base ou non.
Types d’instrumentation
Les principales formes d’instrumentation Linux accessible par le biais de perf
sont:
- Les tracepoint, dont nous avons déjà beaucoup parlé. Il s’agit
de points du noyau Linux qui ont été manuellement instrumentés par les
développeurs de celui-ci, afin de pouvoir suivre quand du code y fait appel
ainsi que la valeur de certains paramètres d’exécution (ex: taille d’une
écriture de fichier pour le tracepoint
syscalls:sys_exit_write
). - Les kprobes sont un mécanisme permettant d’injecter du code en un point arbitraire du source du noyau Linux, pour détecter quand on passe en ce point du code noyau et éventuellement en extraire des informations (contenu de variables, paramètres et résultats de fonctions…). Cela se fait “à chaud”, il n’est pas nécessaire de recompiler son noyau ou redémarrer.
- Les uprobes ont une flexibilité analogue aux kprobes, mais permettent d’instrumenter du code extérieur au noyau (exécutables, bibliothèques partagées…). Leur fonctionnement est analogue à celui d’un point d’arrêt dans un débogueur : le code est interrompu et passe la main au noyau, qui récupère l’information voulue avant de lui rendre la main.
- Les USDT probes, pour User Statically-Defined Tracing, sont un mécanisme basé sur les uprobes qui permet à une application ou une bibliothèque de définir manuellement des points d’instrumentation, à la manière des tracepoints dans le noyau.
Outre le fait que les deux premières ciblent le code noyau et les suivantes ciblent le code applicatif, ces formes d’instrumentation se distinguent par leur performance et leurs garanties de stabilité.
Stabilité
Les tracepoint et USDT probes sont définis par le développeur du code concerné, et sont normalement soumis aux mêmes garanties de stabilité que toute autre API. Par conséquent, on peut s’attendre à ce que l’instrumentation associée reste disponible pendant un certain temps, ce qui permet par exemple de créer des tutoriels ou utilitaires d’analyse de performance basés sur l’information fournie par l’instrumentation.
Les kprobes et uprobes, en revanche, sont définis par la personne qui souhaite instrumenter du code, sans accord préalable avec les développeurs. On n’est pas limité aux interfaces publiques, et on peut instrumenter aussi profond dans l’implémentation qu’on le souhaite. Il n’y a donc aucune garantie de stabilité, et même un petit correctif d’une application peut casser une instrumentation basée sur ces mécanismes. Ils ne sont donc adaptés que pour des analyses ponctuelles.
Performances
L’instrumentation rajoute du code CPU à exécuter à chaque fois que l’exécution passe en un certain point du programme instrumenté, ainsi que du trafic mémoire. Elle a donc un impact sur les performances du code que l’on étudie.
Selon “System Performance: Enterprise and the Cloud” de Brendan Gregg (2e édition, 2020)…
- Les tracepoints définis par le noyau n’ont pas d’impact significatif sur les performances jusqu’à environ ~100k événements par seconde (ce qui suggère un coût de l’ordre de 100ns).
- Le coût des kprobes dépend de leur emplacement dans la fonction noyau
concernée
- A l’entrée d’une fonction (ex: suivi des appels, lecture des paramètres), le coût est comparable à celui d’un tracepoint.
- A la sortie d’une fonction (kretprobe), c’est environ 2x plus cher.
- Au milieu d’une fonction, ça peut être plus cher encore (ex: boucles).
- Les uprobes sont plus coûteuses puisqu’elles nécessitent un point d’arrêt, donc une interruption et un basculement vers le code noyau. On parle de ~13x le coût d’un tracepoint pour une uprobe à l’entrée d’une fonction et ~19x en sortie d’une fonction.
- Les USDT probes sont implémentées via des uprobes et ont donc un coût similaire.
Conclusion
Voici un résumé des informations précédentes.
Instrumentation | Où? | Quand? | Stable? | Coût (Gregg2020) |
---|---|---|---|---|
tracepoint | Noyau | Compilation | Oui | ~100ns |
kprobe (entrée) | Noyau | Exécution | Non | ~100ns |
kretprobe | Noyau | Exécution | Non | ~200ns |
uprobe (entrée) | App/Lib | Exécution | Non | ~1.3µs |
uretprobe | App/Lib | Exécution | Non | ~1.9µs |
USDT | App/Lib | Compilation | Oui | = u(ret)probe |
Notons un dernier point concernant les privilèges et la sécurité. En raison de la très grande dangerosité de l’instrumentation de code arbitraire (risque de ralentir fortement le système, de créer des récursions infinies, de dévoiler des informations qui devraient rester secrètes…), la création de kprobes et uprobes est réservée au super-utilisateur. Vous aurez donc besoin d’un peu d’aide de l’administrateur pour certaines commandes de ce TP.
En revanche, l’administrateur peut rendre ces “sondes” accessibles à l’ensemble
des utilisateurs d’un groupe par une simple commande chown
de ce style :
# Nécessite des privilèges administrateur !
sudo chown -R root:perf /sys/kernel/tracing/events/xyz
Instrumentation kprobe
Comme nous l’avons vu dans la section théorique, les kprobes sont une fonctionnalité du noyau Linux qui permet d’instrumenter n’importe quel point du code source noyau, pour suivre quand ce code est exécuté et éventuellement en extraire de l’information (paramètres, variables…).
Il va de soi que quand on commence à se livrer à ce genre de sorcellerie, la règle d’or du suivi de l’activité système que nous avons introduite précédemment est plus importante que jamais :
Tu ne provoqueras pas, une fois par événement système surveillé, un événement système que tu surveilles également.
Pensez donc toujours bien, avant d’instrumenter une fonction noyau, à vous
demander si elle ne pourrait pas être utilisée par l’outil perf
que vous allez
invoquer. En cas de doute, privilégiez des outils aussi peu “actifs” que
possible (ex: perf record
plutôt que perf trace
).
Choix de fonction
La commande perf probe --funcs
permet d’afficher une liste des fonctions noyau
instrumentables. Sont instrumentables les fonctions qui ne sont pas inlinée et
où l’instrumentation n’a pas été désactivée manuellement par les développeurs
(générallement parce qu’elles sont destinées à s’exécuter dans un contexte où
l’instrumentation poserait problème, ex: gestionnaire d’interruptions).
perf probe --funcs
Début de la sortie de perf probe --funcs
Vous noterez l’utilisation d’un pager. Il ne serait pas judicieux pour moi d’inclure la liste complète, car à l’heure où ces lignes sont écrites (noyau Linux 5.14.21), elle fait 40819 entrées. Ce chiffre est à comparer aux quelques 1600 tracepoints exposés par le noyau, en gardant aussi à l’esprit le fait qu’une kprobe n’est pas non plus forcée de se situer au début ou à la fin d’une fonction noyau…
Comme avec perf list
, il est possible de filtrer la liste de fonctions émise
par perf probe --funcs
en ajoutant un motif shell. Par exemple, on peut
n’afficher que les fonctions qui ont trait aux entrées/sorties asynchrones
“à l’ancienne” (Linux AIO) :
perf probe --funcs '*aio*'
Instrumentation de l’appel d’une fonction
Pour illustrer l’intérêt des kprobes, nous allons maintenant essayer de
surveiller le trafic TCP reçu par le serveur avec perf trace
. Il n’y a pas de
tracepoint adapté à l’heure où ces lignes sont écrites :
perf list 'tcp:*'
En effet, seul le tracepoint tcp:tcp_probe
permet de surveiller le trafic
TCP “normal”, et il ne fait pas de distinction entre le trafic entrant et
sortant. On ne peut donc pas l’utiliser avec perf trace
, puisque chaque sortie
de perf trace
générerait du trafic réseau pour l’envoi via notre connexion
SSH, trafic réseau qui serait lui-même détecté par perf trace
, et on aurait
donc une boucle infinie.
A la place, nous allons instrumenter la fonction tcp_recvmsg()
du noyau, qui
est appelée lorsque du trafic TCP est reçu alors qu’un processus utilisateur
attend des données sur un socket associé. Pour plus d’informations sur
l’implémentation de la pile réseau du noyau Linux, vous pourrez vous référer à
cette page quand vous
aurez accès à Internet.
Signalez à l’administrateur de srv-calcul-ambulant
que vous êtes arrivés à ce
point du TP pour qu’il mette en place l’instrumentation et vous y donne accès si
ce n’est pas déjà fait. Pour votre information, il procèdera comme suit :
# Nécessite des privilèges administrateur !
sudo perf probe tcp_recvmsg \
&& sudo chown -R root:perf /sys/kernel/tracing/events/probe
Notez que vous pouvez consulter la liste des instrumentations actives avec
l’option --list
de perf probe
, qui s’abbrévie en -l
, et que celles-ci
apparaissent également dans perf list tracepoint
.
Exercice : Utilisez perf trace
pour faire un suivi de l’événement
probe:tcp_recvmsg
nouvellement créé, et observez ce qui se passe quand vous
saisissez du texte dans votre terminal. Notez que ce faisant, vous serez aussi
sensible à l’activité réseau issue des autres utilisateurs du serveur.
Capture de variables
A ce stade, nous ne pouvons pas différencier qui est responsable de quel trafic
réseau. Le champ __probe_ip
affiché par perf trace
ne désigne que le
pointeur d’instruction, qui sera toujours le même pour un appel de fonction
ordinaire.
En revanche, avec une kprobe plus sophistiquée, il nous est possible d’enregistrer les paramètres et variables locales de la fonction, et ainsi en savoir plus sur les conditions dans lesquelles elle est appelée. Voyons la liste des variables accessibles :
perf probe --vars tcp_recvmsg
Le pointeur de socket sk
semble être un bon début : il sera différent pour
chaque connexion réseau. Nous allons donc l’utiliser pour raffiner notre suivi.
Vous pouvez afficher également les (nombreuses) variables globales accessibles
depuis le point du code source que vous êtes en train d’instrumenter en ajoutant
l’option --externs
avant --vars
.
Signalez à l’administrateur de srv-calcul-ambulant
que vous êtes arrivés à ce
point du TP pour qu’il mette en place l’instrumentation et vous y donne accès si
ce n’est pas déjà fait. Pour votre information, il procèdera comme suit :
# Nécessite des privilèges administrateur !
sudo perf probe tcp_recvmsg_sk=tcp_recvmsg sk \
&& sudo chown -R root:perf /sys/kernel/tracing/events/probe
Notez la nouvelle syntaxe utilisée. La syntaxe A=B
permet de définir une
probe appelée A qui surveille la fonction B, et les variables qu’on souhaite
surveiller (“arguments” dans le jargon de perf probe
) sont indiquées après un
espace.
Exercice : Reprenez l’exercice précédent avec la probe tcp_recvmsg_sk
nouvellement créée, et utilisez
l’option --filter
de perf trace
pour ne plus observer que les événements TCP issus de votre propre connexion SSH.
Attention, la sortie de perf trace
affiche les pointeurs comme des addresses
entières signées, alors que l’option --filter
attend un entier non signé.
Vous pouvez faire la conversion avec la commande
printf "0x%x\n" <nombre négatif à convertir>
.
Notez qu’une variante simple de cette technique vous permettrait d’exclure
le trafic TCP issu de votre propre connexion SSH, et ainsi d’instrumenter
impunément la fonction réciproque tcp_sendmsg()
qui est appelée à chaque
envoi de données sur le réseau via un socket, sans créer une boucle
infinie liée aux sorties de perf trace
…
Autres emplacements
Jusqu’à présent, pour simplifier les exemples, nos kprobes se sont toujours déclenchées au moment de l’entrée dans une fonction. Mais si nous le souhaitons, il est possible d’injecter une kprobe à peu près n’importe où dans le code de la fonction noyau, ou bien de détecter quand on sort de cette fonction et quelle est la valeur de retour émise à ce moment-là (on parlera alors de kretprobe).
Il est possible d’afficher le code source d’une fonction avec un accent visuel
sur les lignes de code qui peuvent être instrumentées via l’option
--line=<fonction>
de perf probe
, qui s’abbrévie en -L
:
perf probe --line tcp_recvmsg
On voit qu’il n’est pas possible de demander l’arrêt à certaines lignes du code, affichées en bleu. C’est lié au fait que les kprobe travaillant au niveau du binaire compilé, elles doivent être placées au niveau d’une instruction assembleur, or en présence d’optimisations une ligne de code n’est pas forcément clairement associée à une instruction assembleur.
Sur la base du code source affiché ci-dessus, on pourrait par exemple vouloir s’intéresser aux points du code suivant :
- La ligne 20 représente le cas où on a effectué un cycle lock/unlock normal et
s’apprête à exécuter éventuellement des fonctionnalités optionnelles
contrôlées par le champ de bits
cmsg_flags
.- En surveillant la valeur de ce champ de bits, on peut savoir lesquelles de ces fonctionnalités seront utilisées ou pas.
- La sortie de la fonction, qu’elle ait réussi ou échoué (selon une convention
habituelle dans le monde UNIX, l’échec est signalé par un code de retour
négatif).
- En surveillant la valeur du champs de bits
flags
, on peut savoir si l’échec, le cas échéant, est survenu en raison de la conditionflags & MSG_ERRQUEUE
de la ligne 6 ou bien à l’intérieur de la fonctiontcp_recvmsg_locked
appelée ligne 15.
- En surveillant la valeur du champs de bits
En croisant ces deux informations, il est possible d’étudier de façon différenciée les cas où la fonction réussit et ceux où elle échoue.
Signalez à l’administrateur de srv-calcul-ambulant
que vous êtes arrivés à ce
point du TP pour qu’il mette en place l’instrumentation et vous y donne accès si
ce n’est pas déjà fait. Pour votre information, il procèdera comme suit :
# Nécessite des privilèges administrateur !
sudo perf probe tcp_recvmsg_sk_flags=tcp_recvmsg sk flags:x \
&& sudo perf probe tcp_recvmsg_sk_cmsg=tcp_recvmsg:20 sk cmsg_flags:x \
&& sudo perf probe tcp_recvmsg_retval=tcp_recvmsg%return '$retval' \
&& sudo chown -R root:perf /sys/kernel/tracing/events/probe
Notez la nouvelle syntaxe utilisée ici:
- On peut suivre le nom d’une fonction de
:
et un nombre pour indiquer qu’on veut instrumenter une certaine ligne du code, relativement au début de la fonction.- Il est aussi possible d’utiliser cette syntaxe pour instrumenter une
certaine ligne d’un fichier source, par exemple
fichier.cpp:123
.
- Il est aussi possible d’utiliser cette syntaxe pour instrumenter une
certaine ligne d’un fichier source, par exemple
- On peut utiliser des modificateurs comme
:x
pour contrôler l’affichage des variables extraites. Ici, le modificateurx
permet de forcer un affichage hexadécimal, ce qui est plus pertinent pour les données de type “flags”. - On peut instrumenter la sortie de la fonction avec la syntaxe
%return
à la fin du nom de la fonction, et utiliser la valeur spéciale$retval
pour extraire le résultat renvoyé. Cette dernière nécessite un échappement car le signe$
serait sinon (mal) interprété par le shell.- Attention: le noyau ajoute automatiquement
__return
au nom des kretprobe. Ceci vise à permettre d’enchaînerperf probe xyz
etperf probe xyz%return
sans rencontrer d’erreur comme quoi le nom de kprobe “xyz” est déjà utilisé. La kprobe définie ci-dessus s’appelle donctcp_recvmsg_retval__return
.
- Attention: le noyau ajoute automatiquement
Exercice : Utilisez les trois sondes définies ci-dessus
(tcp_recvmsg_sk_flags
, tcp_recvmsg_sk_cmsg
et tcp_recvmsg_retval__return
)
pour en savoir plus sur la façon dont la fonction tcp_recvmsg
s’exécute en
pratique.
Pour une raison mystérieuse, au 13 octobre 2022, perf trace
rencontre quelques
difficultés avec ces sondes et n’affiche pas toutes les variables extraites.
Je vous recommande donc d’utiliser à la place perf record -a
suivi de
perf script
.
Nous reviendrons plus loin sur ce que fait perf script
, mais à ce stade vous
pouvez retenir que si on la lance sans argument, elle ouvre le fichier
perf.data
produit par perf record
et affiche son contenu dans l’ordre
chronologique.
Conclusion
Avec perf probe
, il est possible d’instrumenter presque n’importe quel point
du code source du noyau linux pour savoir quand est-ce qu’il est exécuté et
extraire des valeurs de paramètres, résultats, et variables globales et locales.
Nous allons voir par la suite que ces fonctionnalités d’instrumentation fonctionnent presque pareil pour du code utilisateur (exécutables et bibliothèques).
Mentionnons pour conclure qu’on peut supprimer une kprobe avec la commande
administrateur sudo perf probe --del <nom>
, et qu’il est aussi possible de
spécifier un point d’arrêt par une recherche textuelle dans le code source
(syntaxe ;<glob>
), ce qui permet d’écrire des scripts un petit peu plus
pérennes par rapport aux évolutions futures du noyau Linux. Consultez
perf help probe
pour plus d’informations.
Instrumentation uprobe
Dans la section précédente, nous avons vu comment il est possible d’instrumenter le code du noyau Linux pour en extraire des informations. Nous allons maintenant voir comment une variante de cette instrumentation, les uprobes, peut être utilisée pour extraire des informations du code utilisateur : exécutables, bibliothèques…
Utilisation simple des uprobes
Dans l’ensemble, l’utilisation de perf probe
sur un binaire utilisateur est
très similaire à celle sur le noyau Linux. Il suffit de rajouter un paramètre
--exec
, qui s’abbrévie en -x
, pour désigner le binaire cible. Par exemple,
on peut énumérer les fonctions instrumentables avec --funcs
/-F
.
perf probe -x /bin/bash --funcs
Début de la sortie de perf probe -x /bin/bash --funcs
Si le code source de l’exécutable est disponible (ce qui est le cas pour
bash
sur srv-calcul-ambulant
), on peut afficher les lignes instrumentables
d’une fonction avec l’option --line
/-L
:
perf probe -x /bin/bash --line absolute_pathname
Si des symboles de déboguages sont disponibles, on peut aussi toujours afficher
le nom des variables accessibles par l’instrumentation avec --vars
/-V
:
perf probe -x /bin/bash --vars main
Bref, vous l’aurez compris, à un paramètre -x
près, l’utilisation de
perf probe
pour les uprobes est quasiment identique à celle pour les
kprobes. Notez juste les préconditions : pour avoir toutes les fonctionalités,
il faut avoir accès à la fois à des symboles de déboguage et au code source du
binaire, ce qui est rarement le cas par défaut et nécessite un peu de
préparation de l’administrateur.
Maintenant, puisqu’on parle d’administrateur, invitez-le justement à activer
une instrumentation au niveau de la fonction main()
de bash
avec la commande
suivante :
# Nécessite des privilèges administrateur !
sudo perf probe -x /bin/bash main argc argv[0]:string \
&& sudo chown -R root:perf /sys/kernel/tracing/events/probe_bash
Ce qui est l’occasion d’aborder quelques nouveaux points de syntaxe :
- Il est possible à
perf probe
d’accéder aux éléments d’un tableau C, par contre les pointeurschar*
ne sont pas automatiquement traités comme des chaînes de caractère, il faut explicitement le demander avec le suffixe:string
. - Pour limiter les collisions de noms, les sondes sont regroupées en groupes.
Par défaut, les kprobes appartiennent au groupe
probe
, et les uprobes à un groupe appeléprobe_xyz
où xyz est le nom du binaire instrumenté. Il est possible d’utiliser un nom de groupe personnalisé si on le souhaite, ce qui est par exemple utile si plusieurs personnes avec des droits administrateurs veulent utiliserperf probe
sur le même binaire en même temps sans se marcher sur les pieds.
Exercice : En surveillant la sonde probe_bash:main
nouvellement créée avec
perf trace
, essayez de lancer bash
quelques fois en variant le chemin
d’accès utilisé (détection automatique via PATH vs absolu…) et en ajoutant des
paramètres, et notez comment chaque nouvelle exécution de bash
est désormais
suivie par perf trace
.
Considérations spécifiques aux bibliothèques GNU
Nous l’avons vu, l’utilisation de base des uprobes est, dans l’ensemble, très similaire à celle des kprobes. Toutefois, quand on commence à les utiliser en pratique, on remarque qu’on a besoin de prendre des précautions qui ne sont pas souvent nécessaires avec le code noyau.
Pour être plus précis, lorsqu’on utilise des uprobes, il est très tentant
d’instrumenter des bibliothèques de base telles que la libc
, la libstdc++
(bibliothèque standard C++ de GCC), la libm
(implémentation des fonctions
mathématiques comme sin
, cos
…), ou la libpthread
(implémentation des
primitives de synchronisation comme les mutexes). Mais si on essaie, on se
rendra rapidement compte qu’il ne suffit pas forcément d’instrumenter la
fonction ayant le nom le plus évident (ex: memcpy
dans la libc), pour les
raisons suivantes :
- Les compilateurs tendent à inliner une partie de l’implémentation de ces fonctions au sein du programme appelant, et donc la fonction en elle-même n’est presque jamais appelée, seule des sous-fonctions utilitaires internes sont éventuellement appelées.
- Les bibliothèques GNU utilisent des techniques compliquées pour optimiser la
compatibilité binaire des programmes et le compromis portabilité/performance,
et l’analyse des binaires de
perf probe
a du mal à voir à travers ces couches d’abstraction.
La manière la plus simple de contourner ces aléas est d’utiliser un profileur CPU pour repérer le nom de la fonction qui est réellement appelée par le binaire final, et instrumenter celle-là. Mais parfois, on aura aussi la chance de disposer d’instrumentation statique USDT prête à l’emploi, ce que nous allons aborder dans la section suivante.
Userland Statically Defined Tracing (USDT)
Dans ce chapitre, nous sommes partis des tracepoints exposés volontairement
par le noyau Linux, et nous avons vu comment perf probe
nous permettait
d’instrumenter du code noyau arbitraire (kprobe) ou du code utilisateur
arbitraire (uprobe).
Les personnes attentives à la combinatoire auront remarqué qu’il y a une configuration que nous n’avons pas encore explorée, c’est celle d’une instrumentation exposée volontairement par les binaires utilisateurs. C’est le propos de l’instrumentation USDT que nous allons explorer maintenant.
Recherche d’instrumentation USDT
perf
n’est malheureusement pas capable de découvrir toute l’instrumentation
USDT disponible sur le système par lui-même, car cela impliquerait de rechercher
et ouvrir tous les binaires du système de fichier à chaque appel à perf list
,
ce qui aurait un coût inacceptable.
A la place, perf
conserve un cache de l’instrumentation observée dans les
binaires qu’on l’a amené à ouvrir, par exemple en faisant l’analyse de piles
d’appel acquises via --call-graph=dwarf
.
Il est aussi possible d’ajouter manuellement des binaires à ce cache avec la
commande perf buildid-cache
, ce que nous allons faire maintenant pour toutes
les bibliothèques de bases mentionnées à la fin de la section sur les uprobes,
plus le chargeur de binaires ld
:
perf buildid-cache -a /lib64/libc.so.6 \
&& perf buildid-cache -a /lib64/libm.so.6 \
&& perf buildid-cache -a /lib64/libpthread.so.0 \
&& perf buildid-cache -a /usr/lib64/libstdc++.so.6 \
&& perf buildid-cache -a /lib64/ld-linux-x86-64.so.2
(Vous noterez que chacune de ces commandes affiche un pager vide qui doit être quitté avec la touche “q”. Si vous trouvez un moyen de désactiver temporairement ce pager, je suis preneur.)
Ceci étant fait, l’instrumentation USDT exposée par ces binaires apparaîtra dans
perf list
:
perf list sdt
Sortie de perf list sdt
On voit que les bibliothèques GNU exposent pas mal d’instrumentation de ce type
autour de la gestion mémoire, de la gestion d’exceptions (via throw
/catch
ou
setjmp
/longjmp
), de pièges de performance liés à la libm
, ou de la
manipulation et synchronisation de threads via pthread
.
Activation de l’instrumentation
Si vous regardez la liste ci-dessus, vous constaterez que certains de ces “points d’instrumentation” sont sujets à être fréquemment empruntés, notamment ceux ayant trait à la synchronisation de threads. Il ne serait pas acceptable d’y payer en permanence le coût en performance d’une instrumentation quand celle-ci n’est pas utilisée.
L’instrumentation USDT est donc désactivée par défaut et doit être activée explicitement. Et comme l’implémentation actuelle utilise des uprobes, cette instrumentation doit être effectuée par l’administrateur du système, via les opérations suivantes :
- Remplir le buildid-cache de
root
en relançant les commandes ci-dessus pour cet utilisateur (typiquement viasudo
). - Activer l’instrumentation USDT souhaitée avec la commande
perf probe
, avec la syntaxe utilisée parperf list
.
Comme nous pouvons parfois être amené à activer un grand nombre de points
d’instrumentation à fins exploratoires, c’est le bon moment pour mentionner
qu’on peut passer à perf probe
des motifs shell du style sdt_libpthread:*
.
Invitez maintenant l’administrateur à activer l’instrumentation de la
libpthread
avec le jeu de commandes suivantes :
# Nécessite des privilèges administrateur !
sudo perf buildid-cache -a /lib64/libpthread.so.0 \
&& sudo perf probe sdt_libpthread:* \
&& sudo chown -R root:perf /sys/kernel/tracing/events/sdt_libpthread/
Exercice : Commencez par évaluer la fréquence des différents événements
exposés par libpthread
avec perf stat
, puis prenez un événement suffisamment
peu fréquent et suivez ses occurences avec perf trace
.
perf script
Arrivé au terme de la partie sur perf trace
, vous aurez peut-être été étonné de
voir que je passe très vite sur la possibilité d’enregistrer et rejouer des
traces d’appels système avec perf trace
.
La raison est qu’il existe un outil dédié à l’affichage détaillé de données
mesurées par perf
, qui est plus général et que vous serez donc davantage amené
à utiliser en pratique.
Cet outil, vous l’aurez deviné, c’est perf script
. Et il doit son nom curieux
au fait qu’il peut aussi être utilisé pour simplifier l’écriture et
l’utilisation de scripts manipulant des fichiers perf.data
.
Premier contact
Pour illustrer la première fonction de perf script
(afficher, sous une forme
lisible par des êtres humains, le contenu d’un fichier perf.data
), nous allons
commencer doucement en analysant le fichier perf.data
produit durant l’étude
de perf annotate
:
cd ~/tp-perf \
&& srun make -j$(nproc) \
&& srun --pty perf record ./scale.bin 2048 10000000 \
&& perf script
Sortie de perf script
Pour chaque échantillon enregistré, perf script
nous affiche…
- Le nom du programme qui s’exécutait à ce moment-là
- L’identifiant de processus (PID) associé
- Le moment où l’enregistrement a été effectué, en secondes depuis le démarrage du système.
- L’événement étudié (par défault le passage des cycles processeur) et le nombre de ces événements qui sont survenus depuis le dernier échantillon.
- Le pointeur d’instruction auquel se trouvait le programme au moment où la
mesure a été effectuée, suivi d’une analyse de ce dernier en termes…
- Du symbole (= la fonction) dans lequel on se trouvait, et du décalage en octets de l’instruction active par rapport à ce symbole.
- Du fichier binaire dont est originaire ce symbole.
Ce n’est qu’un sous-ensemble des données qui peuvent être affichées. Avec
l’option --fields
/-F
, on peut demander l’affichage d’autres informations
comme le numéro de thread (tid
), le numéro de CPU (cpu
), ou une estimation
de la ligne de code source associée basée sur les symboles de déboguage
(srcline
). Consultez perf help script
pour plus de détails. Mais pour cette
première approche, nous allons nous contenter des informations affichées par
défaut.
On constate que durant les premières millisecondes enregistrées, on observe
l’activité de perf
lui-même, et pas encore celle du programme enregistré. Il
sera lancé par un appel système exec
une fois la configuration de
perf record
terminée.
Durant cette brève phase, on note un petit couac de mesure (pointeur
d’instruction à l’addresse 0, ce qui est peu plausible), ainsi que ce qui ressemble
à une première calibration du nombre de cycles processeur que perf
doit
compter entre chaque échantillon pour atteindre la fréquence d’échantillonnage
souhaitée (4 kHz pour l’événement cycles
par défaut dans perf
5.3).
Puis l’on entre dans l’exécution du programme scale.bin
proprement dit, où
l’on voit les données brutes qui ont été traitées par perf annotate
pour
produire du code source annoté. On note au passage que la calibration du nombre
de cycles n’est pas complètement terminée à ce stade, et va se raffiner
progressivement au début de l’exécution du programme.
Une première utilité de perf script
est d’examiner ces données brutes
lorsqu’un outil de plus haut niveau, comme perf annotate
ou perf report
,
produit des résultats qui semblent incohérents.
Exercice : Répétez cette analyse avec des données contenant des graphes d’appels DWARF, comme celles produites dans l’étude de l’exemple TPhysics. Notez que cette visualisation se prête plus facilement à l’étude de piles d’appels brisées, dans l’optique de signaler un bug par exemple.
Intel Processor Trace (Intel PT)
Intel Processor Trace, en abrégé Intel PT, est une fonctionnalité intégrée aux processeurs Intel depuis la génération Broadwell qui permet de suivre de façon exhaustive l’exécution d’un programme, à l’instruction assembleur près, ainsi que certains événements CPU associés (branchements, gestion d’énergie, transactions, gestion des interruptions, entrée/sortie d’une VM…).
Comme on peut l’imaginer, le volume de données produit est gigantesque même si elles sont compressées (on parle de plusieurs centaines de Mo par seconde et par coeur CPU). Le coût d’instrumentation peut être conséquent, et l’analyse peut aussi devenir coûteuse (100-1000x plus lente que l’acquisition). Ce type d’étude doit donc être réservé à des programmes dont l’exécution est très rapide, ou bien à des instants courts de l’exécution d’un programme complexe.
Dans cette partie, nous verrons comment acquérir ces données avec perf record
,
les afficher avec perf script
, et les intégrer à une visualisation
perf report
.
Acquisition
Les événements Intel PT sont accessibles via l’événement intel_pt//
. La
syntaxe curieuse //
, que nous n’avons pas abordée auparavant, permet de
spécifier des paramètres pour l’acquisition, par exemple le type d’horloge
utilisé. Elle est aussi utilisée quand on interroge des compteurs de performance
du CPU dont le support n’a pas encore été implémenté au sein du duo perf
+noyau
utilisé, typiquement quand on travaille avec un noyau Linux plus ancien que son
CPU.
Quoiqu’il en soit, les paramètres par défaut seront suffisant pour cette
introduction. Commençons donc par faire une première analyse du code d’exemple
scale.bin
, configuré avec des paramètres permettant d’avoir…
- Une boucle interne courte (64 multiplications -> 8 opérations AVX)
- Un temps d’exécution court (quelques dizaines de millisecondes)
Pour limiter le volume de données produites, nous ne considérerons que
l’activité CPU applicative en ignorant celle du noyau, ce qu’on peut faire avec
l’habituel suffixe u
. Et nous allons nous restreindre à une exécution
séquentielle pour cet exemple afin que la sortie soit plus facile à interpréter.
cd ~/tp-perf \
&& srun make -j$(nproc) \
&& srun --pty --hint=nomultithread \
perf record -e intel_pt//u \
./scale.bin 64 10000000
Affichage détaillé via perf script
En principe, on peut ensuite afficher directement la liste des instructions
exécutées avec perf script --insn-trace --xed
…
perf script --insn-trace --xed
Début de la sortie de perf script --insn-trace --xed
…mais avant d’arriver à l’exécution du code arithmétique qui nous intéresse,
il nous faudrait d’abord passer par des millions de lignes d’assembleur exécuté
par le chargeur de programme ld
et l’initialisation de la bibliothèque
standard. Ce n’est clairement pas viable.
A la place, nous allons commencer par la vue de plus haut niveau fournie par
perf script --call-trace
, qui indique notamment quelles fonctions ont été
appelées, quelles fonctions ces fonctions ont appelé, et ainsi de suite. Un
arbre des appels de fonction visibles du CPU (non inlinés), donc.
perf script --call-trace
Début de la sortie de perf script --call-trace
Cette sortie est encore très imposante (13201 lignes au moment où ces lignes
sont écrites), car ld
et la bibliothèque standard font vraiment beaucoup de
choses au démarrage du programme. Mais nous pouvons exploiter ici le fait qu’il
se passe très peu de choses au moment de l’arrêt du programme (peu de variables
globales avec des destructeurs), sauter directement à la fin de la sortie avec
la touche “Fin” de notre clavier, et remonter avec la touche “page précédente”
jusqu’à la fonction main
, qui n’est pas bien loin en comparaison.
Sortie de perf script --call-trace
autour de main
Sur des exécutions plus complexes où cette astuce ne marcherait plus, il
faudrait utiliser un outil d’analyse de texte (grep
, script python…) pour
repérer la région qui nous intéresse.
Notez, au sein de la fonction main
, une longue période de temps sans appel à
une fonction (la boucle de calcul), où l’on ne voit que des “paquets PSB”
(points de repère utilisés pour l’avance rapide dans le fichier et la
récupération de données partiellement corrompues), des événements de changement
de fréquence d’horloge CPU, et des rappels réguliers du nom de la fonction
active (main
) dont la raison d’être n’est pas claire pour l’auteur de ce TP.
Une fois la fonction main
repérée, nous pouvons repérer la valeur de l’horloge
juste avant qu’on rentre dans la boucle de calcul (ce qu’on repère par une
longue période de temps sans appel à d’autres fonctions) et juste après la fin
de la boucle de calcul (ce qu’on repère par la libération de structures de
données avec free
). Puis on peut relancer la vue détaillée
perf script --insn-trace --xed
en ciblant cette fenêtre de temps avec
l’option --time
:
# ATTENTION: Adaptez la fenêtre de temps à vos données !
perf script --insn-trace --xed --time 402890.894663115,402890.961054471
L’horloge TSC étant relativement peu précise quand on travaille à l’échelle de l’instruction CPU unique, il faut scroller un peu pour arriver à la fin de l’initialisation des vecteurs et au début de la boucle de calcul, mais on finit quand même par trouver notre bonheur :
Sortie de perf script --insn-trace --xed
autour du calcul
(Notez qu’il est possible que les instructions diffèrent un peu sur votre écran
si vous avez fait les exercices du TP sur perf annotate
)
Exercice : Répétez le processus en utilisant cette fois un nombre
d’itérations de boucle interne (premier paramètre de l’exemple scale.bin
) qui
n’est pas multiple de la taille de vecteur SIMD (8 flottants en mode AVX), et
observez comment cela affecte les instructions exécutées. Attention à ne pas
trop augmenter la taille du calcul ce faisant…
Affichages simplifiés
Nous avons vu précédemment qu’il est possible d’afficher un résumé des données
issues de Intel PT qui se limite aux appels de fonction non inlinés et à
quelques autres événements avec l’option --call-trace
. Ce n’est pas le seul
type d’affichage simplifié disponible.
Tout d’abord, il existe une variant de --call-trace
appelée --call-ret-trace
qui permet d’afficher une ligne par retour d’une fonction en plus des lignes
d’appels de fonction. Cela peut être utile quand une fonction appelle
transitivement beaucoup d’autres fonctions, ainsi que pour interpréter des
traces exotiques contenant des appels de fonction “sans retour” (ex: coroutines).
perf script --call-ret-trace
Début de la sortie de perf script --call-ret-trace
Notez les événements tr start
et tr end
qui représentent les moments où
l’enregistrement démarre et s’arrête, typiquement en raison d’un changement de
contexte (rappelez-vous que nous n’enregistrons pas l’activité du noyau dans
notre exemple).
La plupart des autres affichages simplifiés disponibles passent par l’option
--itrace
, documentée dans la page de man perf-intel-pt
, rubrique
PERF SCRIPT
, sous-rubrique New --itrace option
.
Celle-ci permet entre autres de se limiter à l’affichage des instructions i
,
des branchements b
(ou plus spécifiquement des appels c
et/ou retours r
de
fonction), des transactions x
(au sens Intel TSX), et des événements de
gestion de l’énergie p
(typiquement changement de fréquence d’horloge +
paquets PSB). Ces lettres peuvent être combinées pour afficher plusieurs de ces
événements sans afficher la totalité des événements.
On peut également demander, pour les événements sélectionnés, demander
d’afficher des piles d’appels g
pour mieux situer où l’événement se produit,
et demander avec s<N>
où <N>
est à remplacer par un nombre que les N
premiers événements soient ignorés (typiquement pour ne pas décoder et afficher
la phase d’initialisation). Encore une fois, cette liste n’est pas exhaustive.
Le possibilité d’afficher chaque instruction enregistrée avec i
peut sembler
a peu près redondante par rapport à l’option exhaustive --insn-trace
, mais
elle prend tout son sens quand on la combine avec une sous-option qui permet
de n’afficher qu’un échantillon des instructions enregistrées, extrait à la
fréquence de son choix.
Par exemple, --itrace=i1ms
n’affiche qu’une instruction toutes les
millisecondes. Les autres unités disponibles sont us
pour les microsecondes,
ns
pour les nanosecondes, t
à chaque pas de l’horloge utilisée, et i
pour
afficher une instruction toutes les N instructions.
Cette fonctionnalité permet de simuler l’effet d’un profilage CPU à une
fréquence bien supérieure au maximum autorisé par perf record
(quelques
dizaines de kHz), et ainsi d’obtenir des données d’une qualité statistique
inaccessible par d’autres moyens. Ce qui a particulièrement du sens quand on
visualise les données non pas avec perf script
, mais avec perf report
.
Intégration à perf report
Les événements générés par une certaine configuration de --itrace
peuvent être
visualisés non pas sous forme de séquence temporelle, mais d’histogramme/profil,
en passant cette option à perf report
. En voici un exemple qui affiche un
rapport sur les instructions de branchement :
perf report --itrace=b
Après un temps de traitement, perf
nous invite à choisir l’événement qu’on
veut afficher avec une invite où il n’y a qu’un seul choix raisonnable. Gageons
que l’ergonomie sera améliorée un jour…
…puis, si on sélectionne l’événement “branches”, on se retrouve face à une liste des fonctions du programme triée par nombres de branchements…
…et on peut annoter main
pour voir les branchements liés à la boucle de
calcul :
Répartition des branches dans l'assembleur de main
Tout se passe donc comme si on avait fait un perf record -e branches
sauf que
cette fois les données sont une représentation exacte de l’exécution du
programme au lieu d’être une estimation statistique basée sur un échantillonage.
Exercice : Avec les instructions, la possibilité d’échantillonner à la
fréquence temporelle de son choix permet d’avoir un traitement plus rapide tout
en conservant une qualité statistique du résultat très supérieure à ce qui peut
être obtenu par l’utilisation normale de perf record
.
Utilisez la syntaxe --itrace=i<période>
abordée ci-dessus pour obtenir un
profil en nombre d’instructions exécutées de la boucle de calcul simulant une
fréquence d’échantillonage de 1 MHz (1µs entre chaque échantillon). Remarquez
l’excellente qualité statistique du résultat (prévisible en l’occurence, donc
vérifiable), bien que l’exécution enregistrée ait duré moins de 100ms : elle est
équivalente à celle qui aurait été obtenue en échantillonnant à 10kHz pendant
10s !
Paramètres d’acquisition
Nous arrivons à la fin de ce tour d’horizon de ce qu’il est possible de
faire avec Intel PT et perf
. Pour conclure, explorons un peu quels genre de
paramètres peuvent être passés en argument de l’événement intel_pt//
(entre
les deux signes /
).
La syntaxe générale est de donner le nom de chaque paramètre souhaité,
éventuellement suivi du signe =
et de la valeur qu’on veut lui donner (sinon
une valeur par défaut spécifiée dans la documentation), puis on continue avec
les autres paramètres éventuels en séparant par des virgules. Par exemple,
avec cette syntaxe, les paramètres par défauts s’écrivent :
intel_pt/tsc,noretcomp=0/
(activer l’option tsc
, donner la valeur 0 à
l’option noretcomp
).
Les paramètres disponibles dépendent du modèle de CPU que l’on utilise. On peut les répartir en quelques grandes catégories :
- Le choix de l’horloge utilisé. On l’a vu, l’horloge TSC a une granularité un
peu grossière pour du profilage à la granularité de l’instruction CPU unique,
c’est pourquoi d’autres horloges plus précises sont donc disponibles sur
certains modèles de CPU :
- L’horloge
cyc
, si elle est disponible, est précise au cycle CPU près - L’horloge
mtc
, si elle est disponible, offre une précision intermédiaire entrecyc
ettsc
.
- L’horloge
- La désactivation de certaines mesures :
- Le relevé d’horloge (pas nécessaire si on veut juste la séquence d’instructions)
- Le suivi des branches
- L’instruction PTWRITE (qui permet au programme en cours d’exécution d’injecter des données de son choix dans le flux Intel PT)
- Les événements de gestion de l’énergie (ex : changements de fréquence CPU)
- La fréquence à laquelle certains relevés sont fait, notamment celui des horloges non-TSC et celui des paquets PSB qui représentent des points de départ possibles pour le décodage et des points de récupération en cas d’erreur d’acquisition.
- Les options
noretcomp
etfup_on_ptw
qui permettent d’enregistrer des détails supplémentaires, respectivement pour améliorer la résilience aux erreurs d’acquisition et pour mieux localiser la position des instructions PTWRITE dans le flux d’instruction.
Pour plus de détails sur ces paramètres et la détection de leur présence ou non
sur le CPU utilisé, voir la page de man perf-intel-pt
, rubrique PERF RECORD
,
sous-rubrique config terms
.
Plus généralement, la lecture de l’ensemble de cette page de man est recommandée
pour plus d’informations sur Intel PT et son intégration à perf
, cette section
de TP n’étant qu’une introduction aux possibilités offertes par l’outil.
Scripting
Nous arrivons enfin à la fonctionnalité qui a donné son nom à la commande
perf script
, à savoir la possibilité de manipuler les données mesurées par
perf
avec du code.
Bien qu’il serait, en principe, possible de le faire juste avec la
fonctionnalité de base de perf script qui décrit sous forme textuelle les
événements stockés dans un fichier perf.data
, ce serait insatisfaisant pour
plusieurs raisons :
- Il faut disposer d’un fichier
perf.data
, ce qui exclut les analyses “temps réel” sauf à alterner rapidement entre des exécutions deperf record
etperf script
. - On gaspillerait nos cycles CPU à convertir les données issues du fichier
perf.data
en format texte avecperf script
, puis à réinterpréter cette sortie texte sous une forme utilisable par notre programme au sein de celui-ci.
Il est donc préférable, quand c’est possible, d’utiliser une vraie API. En
dehors de l’API noyau perf_events()
utilisée par tous les outils perf
, qui
est accessible depuis tout langage de programmation pouvant invoquer une API C,
deux APIs plus haut niveau sont actuellement utilisables via perf script
, une
pour Perl et une pour Python.
Utilisation de scripts
Nous pouvons commencer par explorer la panoplie de scripts distribués avec
perf
, certains n’ayant pas seulement de la valeur en tant qu’exemple :
perf script --list
Ces scripts se divisent en deux catégories :
- Les scripts d’analyse différée, qui peuvent fonctionner en deux temps :
- On enregistre des données avec
perf script record <script> [<commande>]
.- Si une commande est spécifiée, elle est exécutée et des mesures sont
prises sur le processus associé et ses enfants jusqu’à la fin de
l’exécution (comme avec
perf record <commande>
, mais le choix des événements est automatique) - Si aucune commande n’est spécifiée, on fait des mesures à l’échelle du
système entier (comme avec
perf record -a
). Il est possible de forcer ce mode d’acquisition même quand une commande est précisée avec l’option-a
.
- Si une commande est spécifiée, elle est exécutée et des mesures sont
prises sur le processus associé et ses enfants jusqu’à la fin de
l’exécution (comme avec
- On affiche les résultats avec
perf script report <script>
, suivi des arguments éventuels du scrips mentionnés dans la sortie deperf script --list
.
- On enregistre des données avec
- Les scripts d’analyse temps réel, dont le nom se termine par “top”, et qu’on
lance avec
perf script <script>
suivi des arguments éventuels.- Si on lance un script d’analyse différée avec la même syntaxe,
perf
effectue un cycle enregistrement/affichage sans créer de fichier intermédiaire (les données sont stockées en mémoire). Mais on ne peut pas spécifier d’arguments optionnels.
- Si on lance un script d’analyse différée avec la même syntaxe,
Voici un exemple d’analyse différée avec le script failed-syscalls
, qui
compte les appels système ayant échoués, à l’échelle du système entier si on
ne passer pas de commande en argument :
perf script -a record failed-syscalls sleep 5 \
&& perf script report failed-syscalls
Et voici un exemple d’analyse temps réel avec le script rwtop
, qui indique en
temps réel quels processus transfèrent le plus grand volume de données via les
appels système read()
et write()
, ainsi que les tailles de tampons utilisées
lors des lectures :
perf script rwtop
Notez que certains scripts peuvent avoir des dépendances extérieures non
disponibles sur srv-calcul-ambulant
, par exemple le script sched-migration a
une interface graphique WxPython.
Ecriture de scripts
La liste des noms de langages reconnus peut être affichée avec l’option
--script lang
. Un fichier dont le nom se termine par un de ces noms sera
traité comme étant écrit dans le langage associé.
perf script --script lang
Et la documentation pour un langage donné est contenue dans la page de man
perf-script-<langage>
, par exemple perf-script-python
pour Python. Elle
commence par un tutoriel, suivi de documentation de référence.
On peut générer un squelette de code avec l’option --gen-script=<langage>
,
puis une fois le code écrit, on peut indiquer à perf script
d’utiliser notre
script avec l’option --script=<fichier>
.
Pour en apprendre plus sur l’écriture de script, je vous invite à effectuer le tutoriel contenu dans la page de man associée au langage que vous préférez.
perf iostat
Toutes les commandes perf
que nous avons étudiées jusqu’à présent ont pour
fonction d’étudier le comportement du CPU ou du code qui s’y exécute. Mais
perf iostat
, elle, étudie le volume des échanges entre le CPU et les
périphériques (stockage, GPU, réseau…) via le bus PCI-express (PCIe).
C’est donc par exemple un outil approprié pour savoir si une interconnexion CPU-périphérique est saturée, pour peu que l’on sache quel est le débit crète attendu (ex : PCIe 3.0 x16 = 16 Go/s).
Tout d’abord, on peut obtenir une liste des ports PCIe racine que perf iostat
peut interroger :
perf iostat list
Pour savoir quels périphériques sont raccordés à ces ports, on peut utiliser la
commande /sbin/lspci -D
. Le résultat contiendra un grand nombre de
“périphériques” qui sont en réalité des composants internes du CPU, voici un
affichage simplifié qui ne les inclut pas pour plus de clarté :
/sbin/lspci -D | grep -v 'Intel Corporation Sky Lake-E'
L’adressage PCIe est construit de telle sorte que le numéro de bus (la 1ère paire de chiffres hexadécimaux dans l’adresse) d’un enfant est compris entre celui du port racine auquel il est raccordé et celui du port racine suivant, donc…
- La carte graphique est raccordée au port racine 0000:20.
- Le SSD système est raccordé au port racine 0000:2c.
- Les autres périphériques (USB, SATA, réseau, audio…) sont raccordés au port racine 0000:00.
- Le port racine 0000:14 n’est connecté qu’à des ressources internes au CPU.
Pour illustrer cela, commençons par écrire 1 Gio de zéros sur le SSD par blocs de 4 Mio en surveillant l’ensemble des ports PCIe racine :
srun --pty --exclusive \
perf iostat \
dd if=/dev/zero of=~/.Private/test.dump \
bs=4M count=256 oflag=direct \
; rm ~/.Private/test.dump
On voit dans le rapport produit par perf iostat
qu’aucun des ports PCIe racine
n’a été sollicité de façon significative à l’exception de celui connecté au SSD.
Sur celui-ci, 1026 Mio de données ont été lus par le SSD depuis la RAM (ces données ayant été préalablement préparées par le CPU). On observe donc un petit surcroît de trafic lié au protocole de communication CPU-SSD, mais qui reste très modeste ici.
On voit aussi que les transferts de données utilisent la technique du DMA (Direct Memory Access), où c’est le périphérique de stockage qui fait l’essentiel du travail pour transférer les données. C’est le cas général sur un système moderne.
Pour ce qui concerne le débit, dans le cas présent, l’utilitaire dd
le calcule
pour nous. Mais un intérêt de perf iostat
est que cet utilitaire est
utilisable même sur des programmes qui ne sont pas instrumentés pour mesurer
leur volume de données, en divisant le volume de données transféré par la durée
de la mesure.
On voit en tout cas que le débit est de 2 Go/s, ce qui pourrait saturer
l’interconnexion sur certaines carte mères (ports PCIe 3.0 x2), mais pas toutes
(certaines ont des ports x4). Il faudrait donc avoir les spécifications de la
carte mère et du SSD pour conclure, mais celles-ci n’ont malheureusement pas été
fournies par HP pour srv-calcul-ambulant
.
En revanche, si on fait la même expérience avec le disque dur…
srun --pty --exclusive \
perf iostat \
dd if=/dev/zero of=hdd/$USER/test.dump \
bs=4M count=256 oflag=direct \
; rm /hdd/$USER/test.dump
…on voit qu’on est très loin d’atteindre le débit minimal d’un port PCIe pour la génération 3.0, qui est de 1 Go/s (canal 1x). On peut donc conclure sans spécifications supplémentaires que c’est le disque dur qui limite les performances d’écriture ici, et pas l’interconnexion.
Mentionnons pour conclure que sur des systèmes où il y a beaucoup de ports PCIe
racine (c’est le cas des système en rack, surtout quand ils sont multi-CPU), il
peut être intéressant de n’en surveiller et n’en afficher que quelques uns.
C’est possible de le faire en passant les numéros de port racine à perf iostat
,
avant la commande à surveiller, sous forme de liste séparée par des virgules
(ex : perf iostat 0000:2c,0000:20 <commande>
).
Exercice : En vous inspirant des commandes dd ci-dessus, créez une charge de
travail qui copie 1 Gio de données du SSD au disque dur et surveillez-la avec
perf iostat
en restreignant l’affichage aux ports PCIe auxquels le SSD et le
disque dur sont connectés. Pour plus de réalisme, vous pouvez générer des
données aléatoires en utilisant /dev/random comme source plutôt que /dev/zero.
perf mem
Si vous avez traîné dans les cercles d’optimisation de performances logicielles, vous avez peut-être déjà croisé la phrase “cache rules everything around me”.
En-dehors d’une référence musicale, c’est aussi une réflexion sur le fait que plus le temps passe, plus l’écart de performances entre unités de calcul et bus mémoire se creuse. Ce qui veut dire que pour utiliser efficacement les premières, il faut de moins en moins utiliser le second, et donc tirer de mieux en mieux parti des mémoires cache.
Nous avons déjà vu quelques outils pour analyser l’utilisation qu’on en fait
des caches “à gros grain” (les événements cache et leur suivi via perf stat
et
perf record
). Mais quand on a vraiment envie d’aller dans le détail, ou face à
un code particulièrement complexe, perf mem
est plus puissant.
Là où les formes de perf stat
et perf record
que nous avons abordées se
concentrent sur la fréquence des événements liés au cache et leur localisation
dans le code, perf mem
est focalisé sur les addresses mémoire auxquelles le
code accède, et permet d’établir plus directement quelles données de
l’application doivent en priorité être optimisées pour la localité de cache.
Utilisation de base
Commençons doucement avec notre exemple scale.cpp
, dont la logique d’accès
mémoire est apparente dans le code, ce qui permet de suivre plus facilement ce
que fait perf mem
. Pour la même raison, nous allons le lancer en mode
séquentiel, sans hyperthreading.
cd ~/tp-perf \
&& srun make -j$(nproc) \
&& srun --pty --hint=nomultithread \
perf mem record --ldlat=0 \
./scale.bin 64 1000000000 \
&& perf mem report
perf mem report
affiche alors un menu qui nous permet d’afficher un profil
des lectures…
…et des écritures :
Comment interpréter ces tables ?
- La colonne “Overhead” indique quel pourcentage des échantillons mesurés par
perf
contenaient des accès mémoire avec les propriétés détaillées ensuite, avec pondération par la colonne “Local weight”. - La colonne “Samples” affiche la donnée brute de nombre d’échantillons. Elle n’apporte donc pas de nouvelle information par rapport à “Overhead” mais permet juste de s’assurer rapidement qu’on a une statistique suffisante.
- La colonne “Local weight” attribue un coût relatif à l’accès mémoire, fonction
entre autres choses de la latence d’accès à la donnée pour les lectures.
- Notez que sur les processeurs Intel, on mesure les latences d’utilisation (nombre de cycles écoulés entre le moment où une donnée est demandée par le CPU et le moment où elle est utilisée) et pas seulement les latences de la hiérarchie mémoire (nombre de cycles requis pour rendre une donnée accessible par le CPU). C’est la raison pour laquelle on peut observer des poids très variables sur des codes moins simples.
- La colonne “Memory Access” décrit le type d’accès mémoire (RAM, caches…)
- Les colonnes “Symbol” et “Shared Object” détaillent quelle partie du code a déclenché l’accès.
- Les colonnes “Data Symbol” et “Data Object” l’addresse mémoire ciblée par l’accès.
- La colonne “Snoop” indique la présence de snooping (interception de données émise sur un bus par un autre périphérique connecté).
- La colonne “TLB access” indique si une recherche d’addresse physique a été nécessaire ou si la correspondance virtuelle->physique était en cache (TLB).
- La colonne “Locked” indique si il s’agissait d’accès atomiques Read-Modify-Write. Plus coûteux, ils sont utilisés pour la synchronisation entre threads.
- Je n’ai pas trouvé de documentation sur les dernières colonnes, mes
hypothèses actuelles :
- “Blocked” désigne peut-être les accès invalides qui déclenchent un appel au système d’exploitation (ex : défauts de page).
- “Local INSTR Latency” désigne peut-être à quel point l’exécution de code a été ralentie par cet accès mémoire (dans les cas simples, le CPU peut simplement exécuter d’autres instructions en attendant que les données arrivent).
Dans le cas présent, vous noterez qu’on observe les 8 lectures sur le tas attendus pour un accès à 64 flottants 32-bit en vectorisation AVX (8 flottants par accès), mais aussi deux lectures de la pile du programme inattendus. Ceux-ci sont un effet secondaire des barrières d’optimisation utilisées pour empêcher le compilateur de supprimer tout ou partie des boucles de calcul.
L’inhomogénéité des échantillons pour les accès en écriture est plus surprenante, elle est sans doute liée à des optimisations internes du CPU :
- Deux écritures ciblant une même ligne de cache (bloc aligné de 64 octets sous x86) sont regroupés en une même transaction mémoire à partir du cache L2.
- Une série d’écritures ciblant des addresses mémoire consécutives est un motif d’accès classique, qui est traité de façon optimisée par de nombreux CPUs. Les premiers accès sont alors plus lents que les suivants, le temps que l’optimisation se mette en place.
Exercice : Le paramètre --ldlat=N
de perf mem record
, que j’ai ici forcé
à 0, filtre les données pour ne conserver que les lectures mémoire ayant une
latence supérieure ou égale à N cycles.
En utilisant cette option pour se restreindre à des échantillons intéressants,
augmenter graduellement la taille de boucle interne (premier paramètre de
scale.bin
) tout en diminuant le nombre d’itérations externes (2e paramètre)
pour maintenir le temps d’exécution à peut près constant, et observez le
franchissement des “paliers de cache”.
Vous constaterez rapidement que l’utilisation de perf mem
sur des programmes
non triviaux nécessite la mise en place de logs permettant de suivre
l’emplacement en mémoire des différentes allocations tas effectuées par le
programme, avant de pouvoir interpréter correctement les addresses mémoire
présentes dans la sortie de perf help report
! Sur des programmes bien
optimisés qui font peu d’allocations, cela peut être fait en utilisant un
allocateur mémoire spécial qui affiche la pile d’appel de l’appelant à chaque
allocation.
Utilisation avancée
Les commandes perf mem
sont en réalité un raccourci vers des commandes
perf record
et perf report
un peu complexes.
A l’heure où ces lignes sont écrites, perf mem record --ldlat=<N>
est équivalent
à perf record -e cpu/mem-loads,ldlat=<N>/P -e cpu/mem-stores/P --data --weight
et perf mem report
est à peu près équivalent à
perf report --mem-mode --sort=overhead,sample,local_weight,mem,sym,dso,symbol_daddr,dso_daddr,snoop,tlb,locked,blocked,local_ins_lat
.
L’intérêt de connaître ces commandes brutes est que…
- Côté
record
, cela vous permet d’acquérir d’autres mesures en même temps que le profil mémoire, sur la même exécution du programme. - Côté
report
, cela vous permet d’afficher des profils simplifiés. Par exemple de ne faire que catégoriser les accès mémoire, sans les décomposer pour chaque addresse de données, avec un--sort
n’incluant pas la clésymbol_daddr
. Ou bien d’utiliser d’autres clés de tri.
Exercice : Affichez un profil de scale.bin
ayant pour seules clés de tri
l’overhead, le nombre d’échantillons, le type d’accès mémoire, le symbole où
l’accès mémoire est survenu et le binaire associé, et les propriétés snoop
,
tlb
, locked
et blocked
.
Vous obtiendrez ainsi un résumé de l’activité mémoire du programme un peu plus
détaillé que ce qu’on peut obtenir juste avec perf stat
.
Autres options
Au vu de la parenté de perf mem
avec perf record
et perf report
, il ne
vous surprendra pas que les options de ces dernières commandes soient
supportés par la commande perf mem
associée.
Il y a tout de même quelques options intéressantes spécifiques à perf mem
,
notamment…
- L’option
--type
, qui s’abbrévie en-t
, permet de ne mesurer que des lectures (load
) ou des écritures (store
). - L’option
--dump-raw-samples
, qui s’abbrévie en-D
, permet d’afficher les échantillons dans un format facile à traiter par une machine, à la manière deperf script
. - Les options
--all-user
et--all-kernel
, qui s’abbrévient respectivement en-U
et-K
, jouent un rôle équivalent aux options:u
et:k
dans les noms d’événement : elles permettent de se limiter à l’activité applicative ou à l’activité noyau.
Comme d’habitude, n’hésitez pas à consulter perf help mem
pour plus de détails.
perf c2c
Dans le milieu du parallélisme, il est de notoriété publique que synchroniser des threads a un coût non négligeable, et il est également assez connu qu’un code parallèle qui utilise trop de synchronisation ne sera pas seulement aussi lent qu’un code séquentiel, il sera beaucoup plus lent.
Ce qui est moins connu, c’est que cette lenteur provient surtout de deux mécanismes matériels :
- L’utilisation d’opérations atomiques Read-Modify-Write dans les primitives
de synchronisation, qui sont bien plus coûteuses que des instructions
non-atomiques équivalentes. On a vu que ces opérations atomiques peuvent être
étudiées avec
perf mem
. - La cohérence de cache, qui impose qu’à chaque instant, on ait soit un seul coeur CPU qui a un accès exclusif à une donnée, soit plusieurs coeurs CPU qui y ont accès en lecture seule. Ce qui signifie que quand un thread accède à une donnée écrite par un autre thread, la ligne de cache doit être transférée d’un coeur CPU à un autre. Cette opération a un coût conséquent, d’autant plus élevé que les threads sont sur des coeurs CPUs physiquement éloignés.
La cohérence de cache opère, comme son nom le suggère, avec la granularité d’une ligne de cache, soit 64 octets alignés en mémoire sur les processeurs x86 actuels. Cela signifie que si au sein de ce même bloc de données il y a deux variables utilisées par deux threads différents, même si ces deux threads ne communiquent jamais par le biais de ces variable, on observera une perte importante de performances due au fait que la ligne de cache associée passe son temps à sauter d’un coeur CPU à l’autre à cause de la cohérence de cache.
On parle alors de false sharing, et perf c2c
est un outil conçu pour aider à
étudier ce phénomène.
Introduction
L’exemple pingpong.cpp
met en évidence le phénomène de false sharing dans un
cadre simplifié. Un groupe de N threads partagent un tableau de N octets et
lisent/écrivent chacun à un emplacement dédié du tableau. C’est l’exemple type
de ce qu’il ne faut pas faire, et en dépit de l’absence de synchronisation
après le démarrage du programme l’effet de false sharing sera extrêmement
présent dans cette configuration.
Mesurons donc ce phénomène avec perf c2c
en faisant attention à l’organisation
des données…
cd ~/tp-perf \
&& srun make -j$(nproc) \
&& srun --pty -n1 -c16 --hint=nomultithread \
perf c2c record --all-user \
./pingpong.bin 16 2000000000
Ici, on utilise l’option --all-user
, qui s’abbrévie en -u
, pour ignorer les
conflits au niveau du noyau Linux qui ne nous intéressent pas ici.
Maintenant, affichons les résultats de perf c2c
:
perf c2c report
perf c2c
commence par afficher une table des lignes de cache pour lesquels
plusieurs accès par différents threads ont été détectés. Pour chaque ligne de
cache, sont indiqués…
- Son addresse
- Le noeud NUMA sur lequel elle réside (sur ce système il n’y en a qu’un seul)
- “PA cnt” indique le
nombre de fois où l’addresse physique à laquelle un thread accède a changé entre un échantillon
perf
et le suivant. Ici, les threads passent leur temps à accéder à des offsets différents au sein de la ligne de cache, donc ce nombre est très élevé. - Des statistiques sur le nombre de conflits détectés au niveau du cache L3 ou
de la RAM (HITM = “HIT in a Modified cache line”) :
- Fraction des conflits associés à chaque ligne de cache
- Nombre total de conflits
- Décomposition en conflits locaux (au sein d’un même noeud NUMA) et distant (sur un autre noeud NUMA, plus cher).
- Une distribution des loads et des stores analogue à celle qu’on peut
obtenir avec
perf mem
.
Notez que même dans cette configuration extrême, peu de conflits sont détectés au niveau du cache L3, ce qui est plutôt surprenant. En revanche, il est tout à fait normal qu’aucun accès RAM ne soit détecté, puisque même en situation de false sharing, les données restent dans un cache CPU.
Grâce à l’instrumentation de ping_pong.bin
, on trouve rapidement le tableau
partagé entre les threads (adresse 0x1f15f00) dans cette table. On y voit
qu’environ 5% des lectures échantillonnées par perf
ont dû passer par le Fill
Buffer (interface entre le cache L1 et le cache L2), et que quelques écritures
(0.5%) ne vont pas non plus dans le cache L1, mais dans un niveau de cache inférieur.
Avec la touche d
, on peut obtenir un affichage plus détaillé pour cette ligne
de cache…
…où l’on a le détail des accès observés, triés par offset, région du code concernée et noeud NUMA :
- Les statistiques d’accès sont données en pourcentage des données pour la ligne de cache.
- Pour chaque ligne de cache (“CL” = Cache Line), on a le détail par offset, et ici on a une valeur correcte du nombre d’adresses physiques.
- Puis on voit quel code a accédé aux données (offset brut dans le code source et à droite symbole, binaire et ligne de code source si disponible).
- Au milieu, on a une section “cycles” qui indique le nombre de cycles CPU
cumulés utilisés pour les accès échantillonnés par
perf c2c
. On observe que bien que les accès HITM soient fortement minoritaires dans les mesures, leur contribution est énorme par rapport à celle des lectures normales. - Ensuite on a des données brutes de nombre d’échantillons, pour vérifier la qualité du résultat.
- Puis on a le nombre de coeurs CPU qui ont accédé à chaque offset. Il est
curieux que pour un des offset on ait 2 CPUs ici. Je soupçonne que c’est
soit que
perf
a échantillonné l’initialisation du tableau, soit qu’un thread a migré d’un coeur CPU à un autre. - Enfin, sur la colonne de droite, on aurait, sur un système NUMA, des
information sur le noeud exécutant chaque thread. On peut, avec la touche
n
, basculer dans un mode où les numéros de CPUs concernés sont affichés.
Exercice : Variez le nombre de threads (premier paramètre de l’exemple
pingpong.bin
et observez comment cela affecte le temps d’exécution et les
statistiques de false sharing mesurées par perf c2c
. Vous aurez peut-être
besoin de l’option --show-all
décrite ci-dessous…
Autres options
Par défault, perf c2c report
n’affiche que les lignes de caches pour
lesquelles des accès au cache L3 de type “HITM” ont été détecté. La détection de
tels accès est, on l’a vu, assez rare, donc il arrive fréquemment qu’aucun accès
HITM ne soit détecté, et donc qu’aucune ligne de cache ne soit affichée. Vous
pouvez désactiver ce filtrage avec l’option --show-all
.
perf c2c record
prend des options similaires à perf mem record
, et
perf c2c record --ldlat=<N>
est équivalent à
perf record --weight --data --phys-data --sample-cpu -e cpu/mem-loads,ldlat=<N>/P -e cpu/mem-stores/P
.
On peut également passer des options spécifiques à perf record
après une paire
de tirets --
.
En revanche, contrairement à perf mem report
, perf c2c report
ne peut pas
être imité/reconfiguré avec perf report
.
Parmi les options intéressantes de perf c2c report
, détaillées dans
perf help c2c
, mentionnons la possibilité d’avoir un rapport non interactif
(--stdio
) et celle d’afficher une pile d’appel pour les accès concurrents afin
de mieux comprendre comment on en est arrivé à exécuter chaque code.
Autres commandes
J’ai exclu de ce TP un certain nombre de commandes de la suite perf
,
principalement parce qu’elles me semblaient moins utiles dans la vie de tous les
jours ou parce qu’il était difficile pour X ou Y raison d’en enseigner le
maniement sur srv-calcul-ambulant
.
Dans cette annexe, nous allons cependant faire quand même un point rapide sur ces commandes, leur fonction, et la cause de leur non-inclusion dans le TP.
Gestion des symboles de déboguage
Nous l’avons vu au cours du TP, perf
fait un usage assez intensif des symboles
de déboguage. Ce qui pose quelques problèmes en pratique.
Tout d’abord, sous les distributions Linux actuelles, les symboles de déboguage
des bibliothèques systèmes sont stockés dans des fichiers distincts des
bibliothèques elle-mêmes. Cela permet aux personnes qui n’ont pas besoin de
ces symboles de ne pas les installer dans un premier temps, quitte à revenir sur
cette décision plus tard. Mais cela signifie aussi qu’un outil comme perf
ou
GDB qui utilise des symboles de déboguage doit, à chaque fois qu’il en a besoin,
faire une série de requêtes dans un certain nombre de répertoires dans l’espoir
de les trouver, ce qui peut devenir coûteux quand on s’y réfère souvent.
Pour éviter ces requêtes à répétition, perf
garde en cache l’emplacement où il
a trouvé les symboles de déboguage de tous les binaires ELF qu’il a analysés,
indexés par le build-id, un identifiant interne partagé par le binaire ELF
et ses informations de déboguage. Vous pouvez gérer ce cache au moyen de la
commande perf buildid-cache
.
Un problème connexe est la répétabilité des analyses effectués avec perf
sur
une machine différente de celle où des mesures ont été acquises. Cette
répétabilité est déjà, à la base, restreinte par l’interopérabilité limitée
qui existe entre différentes versions de perf
. Mais quand les informations de
déboguage entrent dans l’équation, le problème devient encore plus complexe,
puisqu’il est difficile de répliquer de façon exacte les versions de binaires
existant sur une machine A sur une autre machine B en utilisant uniquement le
gestionnaire de paquets.
perf
propose donc la commande perf archive
, qui combine un fichier
perf.data
avec les différents symboles de déboguage dont il dépend dans une
archive pour faciliter la répétition d’analyses de performance sur une machine
distante.
Dans une logique similaire, nous pouvons aussi mentionner la commande
perf kallsyms
, qui permet de connaître l’emplacement en mémoire virtuelle où
sont chargés les différents symboles du noyau, ce qui est utilisé en interne par
perf report
lors de l’analyse de l’utilisation CPU du noyau.
Manipulation de fichiers perf.data
Quelques outils permettent de manipuler les fichiers perf.data
produits par
perf record
:
perf data
a vocation a devenir un outil généraliste pour manipuler ces fichiers, mais à l’heure où sont écrites ces lignes il ne peut que les convertir au format Common Trace Format.perf diff
permet de calculer les différences entre les profils de deux fichiersperf.data
. Sur le principe, ça pourrait être très intéressant dans une activité d’optimisation de performances. Mais pour l’instant, cet outil ne travaille qu’en terme de fonctions, (overhead “Self” deperf report
) et pas de graphes d’appels (overhead “Children”). L’information qu’il affiche est donc généralement trop bas niveau pour être utilisable.perf evlist
permet de savoir quels événements ont été enregistrés dans un fichier.perf inject
permet de partir d’un fichierperf.data
et en tirer un second fichierperf.data
contenant quelques données supplémentaires : build-ids des fichiers profilés, événements artificiels indiquant pendant combien de temps un thread s’est assoupi… Pour l’instant, ses fonctionnalités sont très spécifiques à des cas d’utilisation bien particuliers.
Commandes incompatibles avec srv-calcul-ambulant
Les commandes perf
suivantes sont intéressantes dans l’absolu, mais ne peuvent
pas être présentées dans le cadre d’un TP sur srv-calcul-ambulant
car elles
sont incompatibles avec certaines des décisions de conception de ce serveur :
perf ftrace
propose un mécanisme alternatif de suivi des événements système basé sur l’infrastructureftrace
. Cette commande est hardcodée pour n’être utilisable parroot
, et n’est donc pas utilisable par des utilisateurs non administrateurs du serveur.perf lock
permet d’analyser l’utilisation des mutex, mais utilise pour ça des tracepoints qui ne sont pas activés sur les noyaux fournis par la distribution openSUSE que nous utilisons.
Autres commandes semblant peu intéressantes
Ces commandes perf
peuvent être utilisées sur srv-calcul-ambulant
, mais leur
utilité ne semblait pas énorme pour l’audience Reprises, c’est pourquoi elles
ne font pas partie du TP actuel :
perf bench
contient une série de microbenchmarks de diverses fonctionnalités du noyau Linux. Il me semble que d’autres logiciels comme Stress-NG font mieux ce travail.perf config
permet de régler certains paramètres de façon permanente pour ne pas avoir à passer les mêmes flags encore et encore. L’ensemble de paramètres configurables est toutefois quelque peu limité…perf daemon
permet de déléguer l’exécution de commandesperf record
à un groupe de services en tâche de fond. L’intérêt par rapport à une simple utilisation interactive deperf record
ne semble pas évident, sauf peut-être dans une optique d’administration système…perf kmem
permet de mesurer quelques statistiques d’activité de l’allocateur mémoire interne du noyau linux. L’information obtenue est limitée, et je n’ai à ce jour jamais rencontré un problème de performances où il jouait un rôle.perf kvm
permet d’obtenir des informations sur des machines virtuelles hébergées par une infrastructure KVM. Les membres du projet Reprises ont peu de chance d’avoir un jour un accès shell à l’hyperviseur d’une infrastructure KVM, donc il semble peu utile d’aborder cela.perf sched
permet d’obtenir des informations sur l’ordonnancement des tâches et notamment les latences d’ordonnancement, c’est à dire le délai entre le moment où une tâche est exécutable et le moment où elle commence vraiment à s’exécuter. Cette information n’est généralement pertinente que sur des systèmes temps réels ou hébergeant un nombre de tâches actives supérieur au nombre de coeurs CPU, deux configurations qui me semblent peu courantes pour l’audience Reprises.perf timechart
permet de mesurer l’activité CPU et E/S de l’ensemble du système et de la visualiser sous forme d’un énorme graphique SVG avec une ligne par coeur CPU et une ligne par processus système en vol. Mais le résultat est difficilement exploitable, car le SVG résultant est si riche en détails qu’il met à genoux la plupart des outils de visualisation.perf version
ne fait que dupliquer le résultat de la commandeperf --version
à laquelle vous aurez probablement déjà pensé…
Interfaces graphiques
Bien que perf fournisse en standard quelques interfaces graphiques, celles-ci sont… hautement oubliables, et l’on est généralement bien plus avisé d’utiliser les outils en ligne de commande.
Toutefois, ce fort accent sur la ligne de commande peut rebuter certains publics, et plus particulièrement des personnes qui ne sont pas informaticiennes de métier ou de vocation.
Dans cette annexe, nous allons donc interfaces graphiques pour utiliser perf
et plus particulièrement visualiser les données produites.
Hotspot
Quand on fait une recherche sur internet à la recherche d’une interface
graphique pour perf
, l’un des premiers résultats sur lequel on tombe est
Hotspot de KDAB.
Malheureusement, ce n’est pas le choix que je recommanderais en première
intention, car il n’utilise pas le moteur de perf
pour lire les fichiers
perf.data
mais une bibliothèque maison, appelée perfparser
. Or, mon
expérience est que la compatibilité de cette bibliothèque avec les fichiers
produits par perf
est très imparfaite, particulièrement dans le domaine des
graphes d’appels, ce qui conduit à observer des profils corrompus là où
perf report
aurait produit de bons résultats.
La fonction de capture de données est également minimaliste et peu compatible
avec un usage sur serveur (application Qt sans version ligne de commande), donc
on gagnera à enregistrer les données avec une commande perf record
manuelle et
à n’utiliser Hotspot que pour la visualisation. Les auteurs fournissent des
instructions pour cela sur leur README.
Si l’on oublie ces deux limites, Hotspot fournit bien les fonctionnalités
essentielles d’une interface graphique pour visualiser des données issues de
perf record
:
- Visualisation hiérarchique d’un graphe d’appels (flame graph).
- Visualisation temporelle des instants où des échantillons ont été enregistrés (ce qui permet d’identifier facilement des moments où l’application n’utilise pas les CPUs : E/S, attente d’un mutex ou processus fils…) avec possibilité de “zoomer” facilement sur une fenêtre de temps.
- Possibilité de parcourir facilement le graphe d’appels dans les deux sens
(vers les appelants et vers les appelés), alors qu’avec
perf report
il faut plusieurs commandes.
En revanche, on n’aura pas accès à des fonctions plus avancées de perf report
comme la visualisation d’assembleur annoté, l’affichage configurable (option
--sort
). Et le support des tracepoints est anecdotique. Ne vous
attendez donc pas à remplacer perf report
à 100%…
Firefox Profiler
Si l’on accepte le constat qu’il n’existe pas, à l’heure actuelle, de bonne
interface graphique pour effectuer des mesures avec perf
, seulement pour les
visualiser, Firefox Profiler est intéressant.
Bien que cet outil soit à la base pensé pour le profilage de sites web, il est possible d’y importer des profils perf avec des résultats plutôt satisfaisants. Voici un exemple issu de l’expérience Belle 2.
Comme Hotspot, Firefox Profiler supporte la visualisation en flame graph, en
ligne temporelle, et en graphe d’appels bidirectionnel. Et comme Hotspot, Firefox
Profiler ne supporte pas les fonctionnalités plus avancées de perf report
(assembleur annoté, tracepoints, …).
En revanche…
- Le décodage des fichiers
perf.data
est effectué avecperf
lui-même, donc la visualisation est rigoureusement identique à ce qu’aurait produitperf report
. - La visualisation en ligne temporelle est plus ergonomique qu’avec Hotspot, il suffit notamment de passer la souris dessus pour visualiser un échantillon des piles d’appels à un instant T.
- Les menus contextuels (par clic droit) sont très puissants, et permettent notamment de cacher facilement des étapes de la pile d’appels non pertinentes pour l’analyse effectuée afin de rendre la visualisation plus claire pour le non initié.
- Et surtout, la possibilité de partager facilement son profil en ligne sur une
instance hébergée par Mozilla est très précieuse dans les collaborations,
particulièrement quand on doit échanger avec des utilisateurs d’autres
systèmes d’exploitation. En effet, la plupart des autres interfaces graphiques
pour visualiser des profils
perf
sont soit spécifiques à Linux, soit des applications web à installer sur son propre serveur avec toute la lourdeur que cela entraîne.
Autres applications web
Pour explorer quelques autres options, incluant SysProf du projet
GNOME (que je ne détaille pas ici car il
est vraiment trop basique en termes de fonctionnalité par rapport aux deux
autres) et l’import de profils perf
dans KCacheGrind, vous pouvez consulter
la page suivante : https://www.markhansen.co.nz/profiler-uis/.
Installation de perf
Vous n’aurez pas besoin d’installer perf
sur votre ordinateur pour ce TP,
puisqu’il sera effectué sur le serveur de calcul ambulant où j’ai déjà tout
installé pour vous.
Mais vous me remercierez d’avoir écrit cette annexe du TP quand vous essaierez
d’installer perf
sur vos machines plus tard, car il y a un certain nombre de
points auxquels ils faut faire attention, et ceux ci ne sont documentés à aucun
emplacement centralisé…
Prérequis
Version noyau
perf
utilise les compteurs de performance du CPU (Performance Monitoring
Counters ou PMCs en anglais). Cette fonctionnalité est définie au niveau de
la microarchitecture CPU, donc elle peut changer d’une génération de CPU à
l’autre, et le code de perf
devra alors être adapté.
Comme la partie de perf
qui communique avec les PMCs est localisée dans le
noyau linux (module events
), il en résulte que pour éviter tout problème de
support matériel avec perf
, vous devez utiliser un noyau Linux aussi récent
que possible, et au moins aussi récent que votre CPU.
Vous pouvez vérifier votre version de noyau avec la commande uname
…
uname -r
5.14.21-150400.24.21-default
…votre modèle de CPU avec la commande lscpu
…
lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
Address sizes: 46 bits physical, 48 bits virtual
CPU(s): 36
On-line CPU(s) list: 0-35
Thread(s) per core: 2
Core(s) per socket: 18
Socket(s): 1
NUMA node(s): 1
Vendor ID: GenuineIntel
CPU family: 6
Model: 85
Model name: Intel(R) Core(TM) i9-10980XE CPU @ 3.00GHz
... autres infos intéressantes dans l'absolu, mais pas ici ...
…et trouver les dates de sorties correspondantes sur :
Si votre noyau est un peu ancien par rapport à votre CPU, il est possible que
votre distribution Linux vous donne accès à un noyau plus récent. Par exemple,
les versions LTS de Ubuntu peuvent utiliser les noyaux des versions non-LTS via
le mécanisme des Hardware Enablement Stacks (kernels -hwe
). Consultez la
documentation de votre distribution pour plus de détails.
Virtualisation et conteneurs
Dans l’ensemble, pour éviter les problèmes, il est fortement recommandé de
privilégier quand c’est possible l’utilisation de perf
sur des systèmes bare
metal, sans virtualisation ni conteneurs. Néanmoins, avec un peu de travail, on
peut faire entendre raison même à des systèmes virtualisés.
Les compteurs de performance CPU permettent de faire un suivi très fin de l’activité système, et ce suivi peut être malheureusement détourné à des fins d’espionnage. Pour cette raison, de nombreux hyperviseurs de machines virtuelles désactivent l’accès aux PMCs par défaut.
Si vous rencontrez des difficultés dans l’utilisation de perf
sur un système
virtualisé, il est possible que vous soyez face à ce problème. Dans ce cas,
consultez la documentation de votre hyperviseur pour savoir comment autoriser la
machine virtuelle à accéder aux PMCs.
L’utilisation de perf
au sein de conteneurs qui s’exécutent nativement sous
Linux ne devrait pas être sujette à des problèmes de ce type. En revanche…
- La plupart des moteurs de conteneurs empêchent l’exécution de perf par défaut, il faut donner les bonnes permissions pour pouvoir utiliser perf.
- La version de
perf
installée dans le conteneur doit être proche de la version du noyau du système hôte pour éviter des problèmes de compatibilité. - L’utilisation de conteneurs sous macOS et Windows implique l’utilisation d’une machine virtuelle, et est donc concernée par l’avertissement ci-dessus.
Installations
Packaging de perf
Le packaging du profileur perf
varie malheureusement d’une distribution Linux
à une autre. Sauf indication contraire, je vais vous donner ici les instructions
pour la distribution openSUSE, qui seront à adapter pour votre distribution
favorite.
Le minimum vital pour utiliser perf
est bien sûr d’installer le paquet perf
,
qui porte ce nom sur la plupart des distributions utilisant des paquets RPM.
Sous Debian et Ubuntu, il n’existe pas de paquet dédié, il faut à la place
installer les paquets linux-tools-common
et linux-tools-$(uname -r)
.
Je recommande très fortement l’installation de perf
via le gestionnaire de
paquet de la distribution, car il faut s’assurer que la version de perf
installée reste compatible avec la version noyau installée, et l’installation
via le gestionnaire de paquet offre cette garantie automatiquement et la
préserve au fil des mises à jour système futures…
Symboles de déboguage
Applications
Pour pouvoir profiler du code dans des conditions optimales, vous aurez besoin des informations de déboguage et idéalement du code source des programmes que vous comptez analyser, ainsi que pour l’ensemble des autres programmes et bibliothèques qu’ils utilisent de façon transitive.
Sous openSUSE, pour chaque paquet xyz
qu’on veut analyser, il faut installer
les paquets xyz-debuginfo
et xyz-debugsource
associés. Sur des systèmes qui
ont l’analyse de performances comme vocation première, tels que
srv-calcul-ambulant
, il peut être intéressant de prendre l’habitude
d’installer les paquets -debuginfo
et -debugsource
de façon systématique.
Pour les binaires que vous compilez vous-même, il faut demander au compilateur
de générer ces informations. Avec GCC et clang, l’option -g
joue ce rôle. Dans
le cas des projets compilés avec CMake, l’option
-DCMAKE_BUILD_TYPE=RelWithDebInfo
est votre amie.
Noyau Linux
Pour pouvoir profiler l’activité du noyau Linux, vous aurez également besoin des
informations de déboguage et du code source du noyau. Sous openSUSE il s’agit
des paquets kernel-default-debuginfo
, kernel-default-debugsource
et
kernel-sources
, en supposant que vous utilisiez le noyau par défaut de la
distribution (default
).
Désassembleur Intel xed
Pour afficher les instructions assembleur quand vous utilisez
Intel PT, vous aurez besoin de l’outil xed
de
Intel. Celui-ci est rarement inclus dans les distributions Linux, il s’installe
comme ceci :
git clone https://github.com/intelxed/mbuild.git mbuild \
&& git clone https://github.com/intelxed/xed \
&& cd xed \
&& ./mfile.py --share \
&& ./mfile.py examples \
&& sudo ./mfile.py --prefix=/usr/local install \
&& sudo ldconfig \
&& sudo cp obj/wkit/examples/obj/xed /usr/local/bin
Si tout s’est bien passé, à la fin vous aurez un exécutable xed
fonctionnel :
xed | head -3
Premiers tests
perf
fournit une batterie de tests automatisés que vous pouvez lancer avec la
command perf test
. Pour s’affranchir des problèmes de permissions dans un
premier temps, lancez-la en tant que root
:
sudo perf test
1: vmlinux symtab matches kallsyms : Ok
2: Detect openat syscall event : Ok
3: Detect openat syscall event on all cpus : FAILED!
4: Read samples using the mmap interface : Ok
5: Test data source output : Ok
6: Parse event definition strings : Ok
7: Simple expression parser : Ok
8: PERF_RECORD_* events & perf_sample fields : Ok
9: Parse perf pmu format : Ok
10: DSO data read : Ok
11: DSO data cache : Ok
12: DSO data reopen : Ok
13: Roundtrip evsel->name : Ok
14: Parse sched tracepoints fields : Ok
15: syscalls:sys_enter_openat event fields : Ok
... beaucoup d'autres tests ...
Si un test échoue (“FAILED!”) ou ne s’exécute pas (“Skip”), vous pouvez l’exécuter en mode verbeux pour essayer de comprendre pourquoi :
sudo perf test -v 3
3: Detect openat syscall event on all cpus :
--- start ---
test child forked, pid 5998
registering plugin: /usr/lib64/traceevent/plugins/plugin_cfg80211.so
registering plugin: /usr/lib64/traceevent/plugins/plugin_function.so
registering plugin: /usr/lib64/traceevent/plugins/plugin_hrtimer.so
registering plugin: /usr/lib64/traceevent/plugins/plugin_jbd2.so
registering plugin: /usr/lib64/traceevent/plugins/plugin_kmem.so
registering plugin: /usr/lib64/traceevent/plugins/plugin_kvm.so
registering plugin: /usr/lib64/traceevent/plugins/plugin_mac80211.so
registering plugin: /usr/lib64/traceevent/plugins/plugin_sched_switch.so
registering plugin: /usr/lib64/traceevent/plugins/plugin_scsi.so
registering plugin: /usr/lib64/traceevent/plugins/plugin_xen.so
sched_setaffinity() failed on CPU 2: Invalid argument test child finished with -1
---- end ----
Detect openat syscall event on all cpus: FAILED!
Ici, le problème est lié au fait que le test perf essaie d’exécuter du code sur un coeur CPU du serveur ambulant auquel il n’a pas accès, ce qui ne sera pas bloquant en pratique.
Notez que certains des tests de perf
utilisent des fonctionnalités franchement
obscures (ex : exécution de programmes Windows via wine
), et qu’il n’est pas
nécessaire que tous les tests passent pour utiliser perf
.
Contrôle d’accès
Comme mentionné précédemment, perf
permet d’obtenir des informations
extrêmement détaillés sur l’activité système, ce qui représente un risque de
sécurité sur une machine avec plusieurs utilisateurs. Pour éviter tout accident,
la plupart des distributions Linux limitent donc l’utilisation d’au moins une
partie des fonctionnalités de perf
au superutilisateur root
par défaut.
Voici une liste (probablement non exhaustive) de verrous de sécurité qui sont installés par défaut sur les distributions Linux courantes, et de procédures pour les désactiver si nécessaire.
perf_event_paranoid
L’utilisation de base de perf
est contrôlée par le réglage
perf_event_paranoid
du noyau. Ce réglage peut être modifié temporairement en
utilisant la commande sysctl kernel.perf_event_paranoid=x
, ou de façon
permanente en écrivant kernel.perf_event_paranoid=x
dans un fichier du
répertoire /etc/sysctl.d
.
Les valeurs possibles de ce réglage sont :
- 3 : Les utilisateurs non privilégiés ne peuvent pas utiliser perf du tout (réglage par défaut sous Debian, Ubuntu et Android).
- 2 : Les utilisateurs non privilégiés ne peuvent suivre que des processus individuels, et seulement durant l’exécution en mode utilisateur (pas de suivi de l’activité noyau déclenchée par le processus).
- 1 : L’utilisation de perf est toujours restreinte au suivi de processus individuels, mais le suivi de l’activité noyau directement associée au processus (appels système synchrones) est possible.
- 0 : Certaines formes de suivi de l’activité du système entier deviennent possible (ex : suivi des compteurs de performance CPU), mais le suivi à grain fin de l’activité système via les mécanismes de tracing du noyau demeure inaccessible aux utilisateurs non privilégiés.
- -1 : Aucune restriction n’est appliquée au niveau de l’outil
perf
(même si des contrôles d’accès peuvent encore s’appliquer en aval de l’outil, on va le voir plus loin).
Les noyaux récents devraient fournir un
contrôle d’accès à grain plus fin
basé sur les capabilities, permettant par exemple de ne donner accès à perf
qu’à un certain groupe d’utilisateurs. Mais mon dernier test sous Linux 5.14
n’était pas concluant, il fallait encore avoir perf_event_paranoid=-1
pour un
accès complet à perf
.
kptr_restrict
Pour interpréter un profil l’activité CPU du noyau, il est nécessaire de savoir
à quelle partie du noyau on a affaire. perf
mesure la position du pointeur
d’instruction du noyau, mais pour traduire cette information bas niveau en
information symbolique de haut niveau (nom de fonction), il a besoin de
connaître la position en mémoire des différents symboles du noyau.
Cette information est malheureusement également très utile pour mener une
attaque contre le noyau Linux, et elle n’est donc accessible qu’à root
par
défaut. Pour la rendre accessible à tous les utilisateurs, il faut mettre à
0 le réglage kptr_restrict
du noyau.
Là encore, on peut le faire de façon temporaire avec la commande
sysctl kernel.kptr_restrict=0
, ou de façon permanente en écrivant
kernel.kptr_restrict=0
dans un fichier du répertoire /etc/sysctl.d
.
Et là encore, il devrait y avoir moyen de gérer ce contrôle d’accès avec la
capability CAP_SYSLOG
, mais cela ne semble pas fonctionner sous Linux 5.14.
Tracing
Le suivi à grain fin de l’activité noyau (tracing) est une fonctionalité du noyau Linux qui est particulièrement puissante pour l’analyse de performances, mais aussi particulièrement dangereuse sur un système multi-utilisateurs :
- Elle donne trivialement accès à l’ensemble des données passées en paramètres ou reçues en résultat d’appels système par les applications, ce qui peut inclure toutes sortes d’informations secrètes : mots de passe, clés SSH…
- Il est relativement facile de charger fortement le système en l’utilisant, soit en créant une récursion infinie (suivi d’un évènement qui survient lorsqu’on suit un événement), soit en suivant de façon exhaustive un évènement extrêmement fréquent (ex : basculement entre deux tâches).
Cette fonctionnalité dispose donc de ses propres mécanismes de sécurité en sus
de la protection perf_event_paranoid
susmentionnée. Si vous souhaitez la
rendre accessible à un groupe d’utilisateurs, disons le groupe perf
, vous
devez…
- Remonter les pseudo-systèmes de fichier
/sys/kernel/debug/tracing
et/sys/kernel/tracing
avec des permissions 770 (u+rwx g+rwx). - Remonter le pseudo-système de fichier
/sys/kernel/debug
avec les permissions 750 (u+rwx g+rx) pour permettre la navigation jusqu’à/sys/kernel/debug/tracing
. - Transférer la propriété de
/sys/kernel/debug
et/sys/kernel/tracing
au groupeperf
. - Ajuster les permissions des fichiers de
/sys/kernel/debug/tracing
pour faire en sorte que tout ce qui est inscriptible par l’utilisateur l’est par le groupe.
Voici un service systemd permettant d’effectuer ces modifications à chaque démarrage :
[Unit]
Description=Let perf group use ftrace, kprobes and uprobes
RequiresMountsFor=/sys/kernel/tracing
RequiresMountsFor=/sys/kernel/debug/tracing
Before=multi-user.target
[Install]
WantedBy=multi-user.target
[Service]
Type=oneshot
RemainAfterExit=yes
Restart=no
ExecStart=bash -c 'mount -o remount,mode=770 /sys/kernel/tracing && mount -o remount,mode=750 /sys/kernel/debug && mount -o remount,mode=770 /sys/kernel/debug/tracing && chown -R root:perf /sys/kernel/tracing /sys/kernel/debug/tracing && find /sys/kernel/debug/tracing/ -perm -u=w -exec chmod g+w \'{}\' \\+'
ExecStop=bash -c 'mount -o remount,mode=700 /sys/kernel/tracing && mount -o remount,mode=700 /sys/kernel/debug/tracing && mount -o remount,mode=700 /sys/kernel/debug && chown -R root:root /sys/kernel/tracing /sys/kernel/debug'
Si vous utilisez perf probe
, sachez que cette commande peut remettre certains
des fichiers virtuels sous le contrôle de root:root
et que vous serez amené à
refaire des chown
pour rétablir les permissions voulues, comme dans les
exemples “administrateur” de la section perf probe
de ce TP.
Limites
Il pourrait être remarquablement facile de bloquer un système en étant trop
gourmand dans l’utilisation de perf
, par exemple en lui demandant de suivre
chaque cycle CPU, chaque changement de contexte (basculement d’une tâche à une
autre) ou chaque interruption matérielle.
Pour éviter ça, perf
s’impose de rester sous certaines limites d’utilisation
des ressources système, quitte à jeter des données de mesure si ces limites sont
dépassées.
Comme le réglage perf_event_paranoid
, ces limites sont exposées sous forme de
paramètres sysctl
, et peuvent être configurées ponctuellement via la
commande sysctl
ou de façon permanente via le répertoire /etc/sysctl.d
.
Les limites que l’on a le plus de chance d’être amené à ajuster sont…
perf_event_max_sample_rate
, qui contrôle la fréquence d’échantillonnage maximale utilisable avecperf record
.perf_cpu_time_max_percent
, qui contrôle le pourcentage maximal du temps CPU qui peut être employé à traiter des mesuresperf
.
Bien entendu, je ne peux que suggérer de les modifier avec prudence et parcimonie.
Introduction au BPF
Les initiales BPF désignaient historiquement un composant obscur de Linux appelé
le Berkeley Packet Filter, qui permet d’optimiser la performance des outils de
capture de paquets réseau comme tcpdump
en éliminant les paquets réseau non
désirés directement au niveau du noyau, sans devoir les transmettre à
l’application de capture de paquets.
Les programmes BPF subissent des vérifications qui visent à assurer qu’ils ne peuvent pas causer trop de dommages à leur “hôte”. En particulier, ils ne peuvent pas crasher, ni entrer dans une boucle infinie, ni modifier des structures de données ou du code qui ne leur appartiennent pas.
Depuis le début des années 2010, les capacités de cet outil ont été énormément étendues, jusqu’à en faire un outil générique pour injecter du code dans un programme (noyau Linux, applications…). En plus des paquets réseau, il est aujourd’hui possible d’attacher un programme BPF à toutes les sources d’événements abordées dans ce TP (PMCs, tracepoints, kprobes, uprobes et USDT), pour effectuer toutes sortes d’actions en réponse à l’événement.
Les possibilités des programmes BPF ont aussi été étendues, ainsi ils peuvent désormais manipuler des tables de hachage, les partager avec des applications utilisateur, mesurer des piles d’appel, et appeler un petit nombre de fonctions du noyau Linux.
Au niveau des limitations, le nombre d’APIs noyau accessibles est très réduit, les ressources utilisables sont limitées (tant en mémoire qu’en temps CPU), et on ne peut pas faire de boucle dont le compilateur ne peut pas prouver qu’elle est bornée. De plus, en raison d’un grand nombre de failles de sécurité découvertes dernièrement dans l’infrastructure sous-jacente, la plupart des distributions Linux restreignent l’utilisation de BPF à l’administrateur. C’est pour cette dernière raison que je ne peux pas vous en faire manipuler facilement en TP.
En termes d’utilisations concrètes…
- Au niveau de
perf
, on peut injecter des programmes BPF au niveau d’un événement, dont la fonction est de filtrer les événements enregistrés parperf
au niveau noyau, et d’y ajouter éventuellement des données supplémentaires (ce qui est par exemple très utile quand on veut extraires des informations textuelles dans une kprobe/uprobe). - En-dehors du monde
perf
, il est possible de calculer certains types de profils en histogramme directement dans le noyau, sans passer par le processus de copie des données en userspace deperf
et donc d’extraire des informations de façon systématique (à chaque occurence d’un événement) avec un coût plus faible que ce qui est possible avecperf
.
Ces possibilités sont explorées en détail dans le livre de Brendan Gregg BPF Performance Tools.
Références
- Les pages de man de
perf
, accessibles viaperf help <commande>
- Le wiki
perf
- Les exemples de Brendan Gregg
- …et son livre “System Performance - Enterprise and the Cloud”
- …dans l’ensemble, si vous avez du temps, tout son site vaut le détour.