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