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 ?