lundi 18 novembre 2019

Chapitre 73 : Assembleur ARM 64 : instructions SIMD


En 32 bits, je ne suis jamais penché sur l’étude de ces instructions. Je vais donc commencer à voir l’utilisation de ces instructions qui permettent d’effectuer des opérations sur plusieurs données (octets, demi mot, mot) avec une seule instruction.

Dans le programme debutSIMD.s, je commence par tester les instructions les plus simples : mettre une valeur immédiate dans tous les octets du registre v2 (v pour vecteurs), copier l’octet 2 dans l’octet 1 du registre v1 et copier ce registre dans x0 pour afficher son contenu. Ensuite j’effectue une addition pour voir le résultat.
Puis, je me suis demandé à quoi pouvait bien servir ces instructions. Quelques recherches m’indiquent que celles-ci sont souvent utilisées pour améliorer les traitements graphiques. Et je n’ai pas trouvé d’exemples concluants sur internet.
Puis il m’est venu l’idée d’essayer ces instructions pour convertir le contenu d’un registre en une chaine de caractères représentant sa valeur en hexa comme dans la routine prepRegistre16 que j’utilise pour afficher les valeurs des registres. Et ça tombe bien puisqu’un registre 64 bits contient 8 octets qui peuvent être traités par ces instructions.
Mais premier problème : un octet doit être affiché par 2 chiffres hexa par exemple FF et donc pour passer à 2 caractères ascii, il faut éclater l’octet en 2 valeurs. Je n’ai pas trouvé d’instruction SIMD pour faire cela et donc j’ai du extraire d’abord tous les demi octets pairs du registre et les positionner tous les 2 octets dans un registre vecteur de 128 bits. Puis j’ai extrait tous les demi octets impairs et je les ai positionnés tous les 2 octets impairs du registre de 128 bits. Cette manip utilise donc beaucoup d’instructions mais à ce jour je n’ai pas trouvé mieux.
Bon maintenant il faut transformer tous ces octets en caractères ascii. Il faut les tester par rapport à 9 et si la valeur est supérieure ajouter 0x37 pour avoir les lettres A à F sinon ajouter 0x30 pour avoir les chiffres.
Vous remarquerez que pour manipuler les 16 octets du registre 128 bits, on utilise le code v6.16b et pour manipuler 8 octets c’est v4.8b !!

La comparaison simultanée des 16 octets est bizarre au premier abord car le résultat est aussi un registre de 16 octets dont chaque octet contient soit des 0 si la comparaison choisie est fausse soit des 1 si elle est vraie. Puis il faut utiliser une instruction bit pour additionner les bonnes valeurs. Ensuite il ne reste plus qu’à stocker le résultat en mémoire et de vérifier la valeur trouvée. Je ne vous cache pas que je n’ai pas écrit ce programme en 5 minutes !! Il m’a fallu de nombreux tests pour y arriver mais le résultat est ok.
Au bilan il faut 37 instructions pour effectuer la conversion alors que dans la procédure standard il faut effectuer 16 fois une boucle de 10 instructions soit 160 plus quelques instructions de préparation. Mais il faudrait connaitre le nombre de cycles exécutés pour voir s’il y a un gain. Je vais adapter les routines de lecture de temps pour tester l’exécution des 2 procédures.

Dans le programme convhexaSIMD.s je transforme les instructions de conversion précédente en une routine autonome qui prendra dans x0 la valeur à convertir et dans x1 l’adresse de la zone de réception. En vérifiant les instructions, je m’aperçois que l’on peut remplacer les instructions de stockage des 2 registres en mémoire par une seule instruction qui stocke directement les 16 octets du registre de 128 bits. Mais cela nécessite une petite adaptation des instructions qui mouvementent les demis octets car pour un affichage correct il faut inverser les caractères et il n’a pas d’instruction aarch64 d’inversion des octets du registre de 128 bits.

J’ajoute au programme, les 2 sous routines pour prendre le temps de début du test et le temps de fin du test et calculer sa durée. Pour cela dans les routines startChrono et stopChrono j’utilise le call system linux clock_gettime valeur 113 qui permet un calcul en nanosecondes !!!
Pour tester la conversion, j’effectue 10000 fois la routine des instructions SIMD et 10000 fois la routine écrite précédemment pour le vidage des registres. Et pour varier la conversion, je passe le compteur de tests dans le registre x0. 
Les résultats sont concluants, la nouvelle routine met 2 fois moins de temps !!


Plusieurs jours après…. En lisant la documentation et en testant les instructions SIMD, je trouve les instructions trn1, trn2, zip1 et zip2 qui permettent des entrelacements d’octets. Je modifie donc la routine et je la simplifie considérablement grâce aux instructions zip2 et zip2. Le temps d’exécution du programme convhexaSIMD2.s est encore réduit.
Dans le programme suiteInst.s, je teste les autres instructions SIMD disponibles. J’en profite aussi pour vérifier la récupération des paramètres de la ligne de commande.
Les autres utilisations s’avèrent assez décevantes compte tenu de la complexité pour manipuler les octets de chaque registre.

3 commentaires:

  1. Comme tu parle d'optimisation , il faut savoir que sur certain processeur ARM sont superscalaire in order.
    Pourquoi je dis cette précision , si tu code sur un proc out of order ,ce que je vais dire ne te servira pas ;)

    Mais si c'est un processeur superscalaire in order , cela signifie que tu peux atteindre la sacro sainte 2 instructions par cycles !

    si je prend un exemple au hasard sur ton code :
    movi v2.8b,10 // met 10 dans chaque octet du registre
    mov v1.b[1],v2.b[2] // met l' octet N° 2 dans l'octet N°1
    Dans les fait ça pourrait faire 2 cycles pour les deux , mais comme il y'a une dépendance entre elle ,tu te prend le maximun de cycles (pipeline stall) , cycles correspondant au nombres étages ! :)

    Bien sur le mieux c'est de voir les instructions compatibles entre elle , ça te permettra d'atteindre sur les points critique 2 instructions/cycles (sur les boucles faudra penser un peu à la déplier) pour éviter tout dépendance de données /pipeline.

    RépondreSupprimer
  2. Merci de ce commentaire. Je vais me renseigner sur les processeurs arm superscalaire in order ou out of order car je ne connais pas cette notion.
    Sur l'optimisation des instructions, tu as parfaitement raison mais je trouve que c'est difficile d'organiser les instructions pour diminuer le nombre de cycles. Car les instructions sont le plus souvent dépendantes.
    Cdlt.

    RépondreSupprimer
    Réponses
    1. Alors oui , c'est pas une optimisation facile d'éviter les dépendance ;)
      Mais c'est faisable avec pas mal d'expérience ah ah :D

      Y'a quelque astuce pour ça par exemple la plus simple faire tout le load au début, ensuite toute les exécutions et finir en store.
      En général avec 4 load , la pipeline (du premier load) normalement est fini/presque fini.

      Supprimer