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.

samedi 16 novembre 2019

Chapitre 72 : Assembleur ARM 64 : tests instructions, nombres aléatoires, saisie chaine et nombre


Dans ce petit programme, testRegSup.s, je vérifie les instructions add avec le mot clé supplémentaire UXTW ou SXTW qui permettent d’additionner des registres de tailles différentes avec en plus la possibilité d’effectuer une multiplication du registre avant son addition.
Je teste aussi le même mot clé pour les accès mémoire. Et enfin je réecit en 64 bits la routine de génération de nombre aléatoires. Je ne me souviens plus d’où j’ai tiré la première série de nombres servant au calcul !! mais j’ai mis en commentaire une deuxième série tiré d’un livre sur les algorithme en langage C de Robert Sedgewick.

Dans le programme suivant saisie.s: https://github.com/vincentARM/ARMassembly64/blob/master/projet64_003/saisie.s, je teste la saisie d’une chaine de caractère en utilisant le call système Linux Read. Comme il faut un buffer de réception de la chaine saisie et à la place d’une zone réservée dans la .bss, je reprends la procédure d’allocation sur le tas avec le call system Linux BRK. Cette procédure effectue 2 appels, le premier pour récupérer l’adresse du tas et qui sera donc l’adresse de début de notre zone réceptrice et l’autre qui alloue la place demandée. Vous remarquerez que la procédure alloue une place de telle façon que la fin soit sur une frontière de 8 octets suivante. En effet lorsque je mettrais cette sous-routine dans le fichier routinesarm64, il faudra qu’elle fonctionne pour tous les appels futurs et en particulier pour des utilisations du tas pour des tables de double mots alignées sur une frontière de 8 octets.
Le test de la saisie chaine étant ok, je teste la conversion d’une chaine en un nombre positif ou négatif.
Petite curiosité : quand j’ai écrit cette routine en 32 bits au début de ma découverte de l’assembleur arm, j’effectuais la conversion de la saisie en nombre négatif en multipliant le résultat par – 1 . ensuite j’ai découvert que l’on pouvait utiliser les 2 instructions mvn r1,r1  et add r1,#1 et maintenant en 64 bits la seule instruction :    cneg x0,x0,eq
Vous remarquerez qu’au fur et à mesure de ma découverte de l’assembleur 64 bits, j’adapte les routines pour utiliser les nouvelles instructions : par exemple pour tester un registre à zéro : l’instruction cbz ou son contraire l’instruction cbnz. Pour gérer éventuelle erreur de calcul, je positionne le carry avec les instructions cmp x0,0 et cmn x0,0. Vous pouvez vérifier que quel que soit la valeur de x0, le carry est ou non positionné dans tous les cas : astucieux non !!

jeudi 14 novembre 2019

Chapitre 71 : Assembleur ARM 64 : accès mémoire


Dans les précédents programmes, nous avons déjà rencontrés les instructions d’accès à la mémoire (par exemple avec les sauvegardes de registres sur la pile, l’adresse de la pile n’étant qu’une adresse d’une zone mémoire).
Dans le programme instLdrStr, nous commençons avec les instructions de stockage d’un octet, demi mot mot et double mot. On remarque que c’est la même instruction str qui sert pour un mot et un double mot mais c’est le registre d’origine (x ou w) qui fait la différence. Puis nous lisons les mêmes informations avec l’instruction ldr. J’ai teste aussi l’instruction ldur mais je n’ai pas vu de différence avec ldr. C’est à approfondir.
Pour les accès avec déplacement, j’ai eu une erreur de compilation avec l’instruction str x0,[x1,x2,lsl 2] que j’ai eu du mal à résoudre. En fait la valeur pour un double mot ne peut être que supérieure à 2 (mais pour un mot, elle peut être égale à 2). Je teste aussi la possibilité d’avoir un déplacement avec un indice contenu dans un registre 32 bits grâce au mot clé supplémentaire UXTW (voir la documentation pour d’autres possibilités).
Ensuite nous restons les lectures d’octet négatif avec recopie du bit de signe. Il reste encore des instructions particulières à tester.
Nous terminons le programme par un exemple simple de manipulation d’une table avec des enregistrements comportant plusieurs champs. Nous décrivons les champs d’un enregistrement à l’aide d’une structure (.struct), nous déclarons dans la section .bss la place nécessaire pour gérer 10 enregistrements.
Dans le code nous écrivons une routine pour insérer un nouvel enregistrement dans la table et une routine pour afficher un enregistrement. L’adresse de chaque enregistrement est calculée grâce à l’instruction madd qui permet d’ajouter à l’adresse de la table, la longueur d’un enregistrement * numéro de l’enregistrement.
Ici, pour chaque insertion, nous balayons toute la table pour trouver un emplacement vide, mais nous aurions pu stocker l’enregistrement directement dans le dernier poste vide puisque nous avons le registre x28 qui contient le nombre d’enregistrements déjà stockés.
Vous remarquerez que j’utilise les instructions ldrsb x0, ldrsh x0 et ldrsw x0 à la place de ldrb w0,ldrh w0 et ldrw w0. Ces instructions complètent le registre 64 bits en dupliquant le signe.

dimanche 10 novembre 2019

Chapitre 70 : Assembleur ARM 64 : tests de quelques instructions 64 bits


Maintenant, que les outils précédents sont prêts, nous allons regarder plus en détail les principales instructions disponibles en 64 bits. Le programme instrBits.s va tester les instructions de manipulations de bits. Nous commençons par les tables de vérité and, oor, eor, orn, eon et bic. Cette dernière est intéressante pour remettre des bits à zéro.
Par exemple dans la routine d’affichage mémoire, l’affichage commence toujours en début d’un bloc de 16 octets soit une adresse qui se termine par 0 hexa soit 0b0000  binaire. Pour remettre ces 4 bits à zéro j’utiliser les instructions mov x1, x2, ASR #4 puis mov x1, x1, LSL #4 alors que l’on peut avoir une seule instruction :
Bic x1,x2,0b1111     (voir l’exemple bic_bis dans le programme instrBits.s)

Ensuite nous testons les instructions lsr,lsl,ror et asr. Asr permet de diviser un nombre négatif par une puissance de 2 à faible côut. Ce test m’a perturbé car l’affichage du résultat en nombre décimal était incorrect. Après recherche je me suis aperçu que je passais dans x1 l’adresse de la zone réceptrice avec l’instruction mov x1, qAdrszZoneRec1 à la place de ldr x1, qAdrszZoneRec1. En 32 bits, le compilateur signalait une erreur mais pas en 64 bits. Dans ce cas, il charge bien une valeur dans x0 mais je n’ai pas encore trouvé à quoi elle correspond. Cela va être une source d’erreur pas facile à trouver.
Nous testons les comptages de bits à zéro ou de un  à gauche avec les instructions clz et cls puis les inversions de bits, d’octets, de demi mot et de mot et nous terminons par les remplacements de bits.
J’ai mis un exemple de calcul de valeur absolue sans utilisation d’un test !! et je termine par un test d’une routine dans laquelle il n’y une instruction qui désaligne la pile. Si vous enlevez le commentaire vous verrez apparaitre l’erreur erreur du bus !!!

Dans le second programme instMovArith.s, nous allons tester les instructions arithmétiques et move. Je commence par refaire le test de l’instruction mov x0,label pour voir à quoi correspond le résultat. Il s’avère que x0 contient maintenant le déplacement du label à partir du début de la section .text.  Si le label fait partie de la section .data, le déplacement est celui du début de cette section. Cela peut être utile de le savoir et il faudra faire quand même attention à ne pas confondre avec ldr x0,label.

Dans la suite de programme, je teste la valeur immédiate maximum acceptable dans un mov. La longueur maxi est de 16 bits soit une valeur égale à 0xFFFF soit 65535 décimal. S’il faut manipuler une valeur immédiate supérieure il faut utiliser l’instruction movk avec un déplacement de 16,32 et 48 pour positionner des tranches de 16 bits au bon endroit.
Puis je reteste l’instruction movn qui ne donne pas la valeur d’un complément à 2 mais qui est en fait l’operateur not. Par contre dans la documentation je trouve l’instruction neg qui donne bien le résultat attendu : neg x0,-5 donne 5
Ensuite, je teste l’instruction addition en vérifiant la mise à jour du flag overflow pour une valeur supérieure à 2puissance 63. Puis je teste la soustraction et la multiplication.
Curieusement pour la multiplication, il n’est pas possible d’ajouter le s pour demander la mise à jour du registre d’état et donc de vérifier l’overflow. Il faut donc recourir à une astuce : utiliser l’instruction smulh pour multiplier les parties hautes des registres et si le résultat est non nul c’est qu’il y a dépassement de capacité.
Dans le programme instrCond.s, nous allons vérifier quelques instructions conditionnelles. Tout d’abord on teste l’instruction csel qui permet de mettre dans un registre la valeur d’un deuxième ou d’un 3ième registre suivant la condition. En fait c’est un if then else mais concernant qu’une seule possibilité.
La 2ième instruction testée est cinc qui incrémente ou non un registre suivant la condition.
Puis je teste l’instruction csinc qui soit incrémente un registre soit met la valeur d’un autre.
Puis l’instruction cneg qui permet de calculer une valeur absolue en 2 instructions seulement.
Et je termine par l’instruction ccmp plus compliquée : ici je montre en 2 exemples comment vérifier si une valeur est comprise entre 2 bornes incluses puis entre 2 bornes exclues.  J’ai mis zéro pour la 3ième donnée de l’instruction car je n’ai pas bien compris son rôle.
Les sources des programmes se trouvent dans ce répertoire :