samedi 12 octobre 2019

Chapitre 65 : mesure de performance du Cortex m53 (pi3b+) avec PERF_EVENT_OPEN


Dans les chapitres concernant le développement baremetal, j’avais utilisé les instructions spèciales mcr et mrc pour afficher le compteur du nombre de cycles (voir chapitre ..) mais cette solution ne peut pas être utilisée avec Linux car ces instructions sont interdites !! même en utilisant sudo.
En cherchant sur internet, des informations sur le processeur CORTEX M53 qui équipe mon raspberry pi3b+, je suis tombé sur un article en anglais qui expliquait la possibilité d’utiliser le périphérique PERF_EVENT disponible sous Linux pour effectuer toute une série de mesures. J’ai donc creusé un peu plus la question à partir de la documentation disponible sous ce lien :
Bien sûr, documentation en anglais mais très complète et qui se terminait par un programme en C. J’ai donc repris ce programme, pour le réécrire en assembleur ARM et voir si celui-ci donnait des résultats exploitables.
Le premier programme m’a donc servi à effectuer une série de tests pour utiliser perf_event_open sur le raspberry.
Il a donc fallu retrouver la structure de paramétrage nécessaire (perf_event_attr),et les constantes nécessaires.
La structure et les compteurs résultats sont en 64 bits, ce qui complique un peu la tâche.
Dans le fichier /usr/include/arm-linux-gnueabihf/asm/unistd.h j’ai retrouvé le code de l’appel système à utiliser : 364 pour le perf_event_open.
Le programme commence par la mise en place des paramètres dans la structure : PERF_TYPE_HARDWARE dans le type, la longueur de la structure puis les bits de contrôle : disabled à 1 pour lancer la mesure à l’aide d’un appel ioctl, et exclusion des instructions du kernel et du superviseur.
L’adresse de la structure sera passée dans le registre r0, puis le pid ou zéro dans le registre r1 puis le n° de la cpu à suivre (ici nous demandons toutes les cpus) dans r2 puis -1 dans r3 (distingue un leader dans un groupe) puis le compteur désiré dans le registre r4. Nous allons compter le nombre d’instructions (ou de cycles compteur qui est en commentaire) Puis nous effectuons l’appel système 364 et s’il n’y a pas d’erreur, nous récperons le File Descriptor qui sera associé au périphérique.
Pour lancer et arrêter la mesure, nous utilisons l’appel système ioctl tout d’abord pour effectuer un reset puis pour autoriser la mesure. Puis nous effectuons les instructions à mesurer et nous arrêtons la mesure par un nouvel appel ioctl.
Cela permet de ne mesurer que les instructions que nous voulons. Le résultat est lu par un simple appel système read auquel nous passons le FD crée plus haut et l’adresse du buffer. Le résultat se trouve dans les 8 premiers octets du buffer. Pour l’instant nous nous contentons de convertir les 4 premiers octets en chaine de caractères que nous affichons par la procédure habituelle.
Si les paramètres ne sont pas ceux attendus, les appels génèrent des erreurs pas toujours faciles à trouver !!
Commençons par compter les instructions pour un simple mov. Le résultat est 9 ce qui semble correspondre à toutes les instructions entre les 2 svc. Remplaçons le mov par un bl appelProc pour appeler une procédure qui effectue le minimum : nous trouvons 15 ce qui correspond bien au nombre d’instructions.
Remplaçons  le compteur par celui de mesure des cycles et relançons le programme plusieurs fois, le résultat n’est pas identique à chaque lancement. J’avais déjà constaté ce phénomène lors des tests hors linux mais je n’ai toujours pas d’explication. Je suppose que la présence de mémoire cache, l’anticipation des instructions, la présence de 4 cœurs font que le nombre de cycles est variable  pour les mêmes 15 instructions !! Donc cela ne sera pas facile d’optimiser une routine particulière. Il faudra peut être effectuer des milliers d’appel de la routine et de calculer des moyennes.
Maintenant, vous pouvez tester d’autres compteurs et sélectionner un cœur particulier ( ou cpu)  !! Voir la documentation bien fournie sur toutes les possibilités. Je n’ai par contre pas pu faire fonctionner le compteur PERF_COUNT_HW_REF_CPU_CYCLES mais il faut peut être ajuster d’autres paramètres.
Dans cette doc, j’ai trouvé qu’il était possible de regrouper plusieurs compteurs pour effectuer les mesures sur la même entité. C’est l’objet du deuxième programme.
Dans ce programme, nous créons un leader comme dans le premier programme en ajoutant le paramètre PERF_FORMAT_GROUP pour grouper les résultats puis nous créons 2 types de mesure fils en passant le FD du leader dans le registre r3 (pour le leader, nous avons laissé -1). Par ailleurs nous mettons le bit diseabled à zéro car comme cela c’est le démarrage du leader qui lancera les 2 fils.
Le compteur du leader mesurera le nombre d’instructions, le fils1 mesurera le nombre de cycles et le fils2 le temps d’exécution (PERF_COUNT_SW_TASK_CLOCK). Attention pour ce compteur il faut mettre le paramètre type à PERF_TYPE_SOFTWARE.
Ensuite comme précédemment, nous lançons la mesure avec uniquement le FD du leader à l’aide des appels système IOCTL puis nous récupérons les résultats en effectuant un seul read. Les tests indiquent qu’il faut une longueur de 8 * 4 = 32 octets pour le buffer de lecture. La documentation indique que la première zone contient le  nombre de compteurs (ici nous trouvons 3) puis les 3 compteurs stockés dans les 3 zones de 8 octets suivantes. Nous convertissons ces zones en appelant la routine de conversion d’un double de 64 bits pour être rigoureux.
Remarque : pour le paramètre read_format nous pouvons ajouter PERF_FORMAT_TOTAL_TIME_ENABLED et/ou PERF_FORMAT_TOTAL_TIME_RUNNING. Dans ce cas, il est ajouté après les 8 octets du nombre de compteurs, une ou deux zones de 8 octets contenant les temps d’exécution. Un test montre que ces temps sont équivalent au compteur PERF_COUNT_SW_TASK_CLOCK pour des instructions ou procédures courtes.
Après quelques tests, le nombre d’instructions semblent stables alors que le nombre de cycles et le temps sont très variables. Pour certaines procédures, le nombre de cycles est inférieur au nombre d’instructions ce qui me parait bizarre.


A titre d’exemple le programme instSpecgrM1 montre la métrologie effectuée sur une procédure de conversion en ASCII d’un nombre de 64 bits (double). Nous partons de celle proposée dans le chapitre 54 et nous l’appellerons 1000 fois dans une sous-routine.
Tout d’abord nous effectuons une première mesure en mettant les appels en commentaires.  Sur mon raspberry pi3+ nous trouvons 8009 instructions ce qui correspond aux 8 instructions à l’intérieur de la boucle exécuté 1000 fois (y compris les instructions propres à la boucle) et 9 instructions hors boucle et compris entre les 2 svc de mesure.
Effectuons une mesure avec la première procédure et nous trouvons 14 941 894 instructions exécutées : waouhh !!
Une première optimisation est faite en remplaçant la division générale d 'un mot de 64 bits par un mot de 32 bits par la division d’un mot de 64 bits par une constante et qui n’utilise que des décalages et une multiplication pour le calcul du reste. Nous trouvons 8 263 009  instructions soit un gain de 44%/
Une deuxième optimisation pour simplifier les décalages de plusieurs positions à droite, supprimer les tests d’overflow et remplacer la multiplication de 64 bits par une simple instruction de multiplication de la partie basse (car il s’agit de calculer le reste qui ne peut être qu’inférieur à 32 bits). Nous trouvons 1 745 342 soit un gain de 88% . Pas mal non !! 
Conclusion : il faut commencer à chercher le meilleur algorithme pour résoudre le problème particulier puis optimiser ce qui peut l'être plutôt que de commencer par optimiser un algorithme non performant !!!
 

Aucun commentaire:

Enregistrer un commentaire