12

Neon Instruction : VTBL, VTBX

fév
9 Comments » |  Posted by Etienne SOBOLE |  Category:ARM, Assembleur, Code

J’ai décidé de commencer une nouvelle série de posts pour détailler le fonctionnement de certaines instructions de NEON. Certaines instructions sont effectivement tellement particulières que lorsque je dois les utiliser à nouveau, je passe déjà une heure à me rappeler comment elles fonctionnent. Du coup, autant passer un peu de temps à faire une doc bien clair (pour moi ;) ) avec en plus quelques cas où j’ai eu besoin d’utiliser ces instructions.
Les premières à y passer sont les instructions VTBL et VTBX.

Introduction

Présentation

VTBL.8 Dd, {Dlist}, Dm
VTBX.8 Dd, {Dlist}, Dm

L’instruction VTBL permet d’extraire jusqu’à 8 octets dans un ensemble de registre Dn (consécutifs) puis les stocke dans le registre destination.
Cette fonction permet une permutation très souple des octets puisque les indices des octets extraits sont fournis dans le registre Dm.
Si l’indice de l’octet fourni sort de la plage possible, alors l’octet destination :

  • vaut 0 si on utilise l’instruction VTBL
  • reste inchangé si on utilise l’instruction VTBX
Encodage de l’instruction

  • o (bit 6) : permet de définir s’il s’agit de l’instruction VBTL ou VTBX
  • D:Vd (bit 32 et 15 à 12) : représente le registre destination Dd
  • M:Vm (bit 5 et 3 à 0) : représente le registre d’indice, lequel contient les numéros de octets à récupérer.
  • N:Vn (bit 7 et 19 à 16) : représente le premier des registres composant la liste des registres source.
  • len (bit 9 et 8 ) : indique le nombre de registre Dn contenu dans la liste (entre 1 et 4).

On se rend donc compte de certaines limitations de l’instruction

  • On ne peut pas utiliser des registres 128 bits Qd et Qm (ce qui est très dommage)
  • La liste des registres Dlist est formé d’un maximum de 4 registres Dn consécutifs
  • Il n’est pas prévu de déplacer autre chose que des octets. Il faut dire que déplacer des demi-mot (ou des mots) reviens à déplacer 2 (ou 4) octets consécutifs.
Fonctionnement

Nous voilà finalement à la partie la plus importante. Mais comment fonctionne cette instruction.

VTBL.8 Dd, {Dlist}, Dm
VTBX.8 Dd, {Dlist}, Dm

Ici Dlist est composé de 4 registres Dn.
Ces 4 registres doivent être vus comme une succession de 32 octets dont les indice vont de 0 à 31.
L’octet de poids faible du registre Dn (valant 5 dans l’exemple) est l’octet dont l’indice vaut 0.
L’octet de poids fort du registre Dn+3 (valant 195 dans l’exemple) est l’octet donc l’indice vaut 31.

Le registre Dm contient une liste de 8 indices d’octet que l’on souhaite extraire de Dlist et placer desn Dd.

L’instruction VTBL va donc récupérer les octets de Dlist correspondant (en rouge) aux indices fournis dans Dm (en vert) et les copier dans Dd.
On remarque que l’indice valant 52 est hors plage, puisque le plus grand indice possible est 31.
Dans ce cas comme aucun octet ne représente cet indice:

  • VTBL va mettre 0 dans le registre destination à la place de l’indice manquant
  • VTBX ne modifiera pas la valeur de l’indice manquant dans le registre destination Dd

D’autres explications concernant cette instruction sont fournies sur le blog d’ARM .

Cycles

Bon, c’est là que ça se complique pour l’instruction VBTL (et VBTX).
Ces instructions sont certes puissantes, mais elles ont un coup non négligeable.

Si Dlist contient 1 ou 2 registres Dn alors l’instruction s’exécute en 2 cycles.
Si Dlist contient 3 ou 4 registres Dn alors l’instruction s’exécute en 3 cycles.

Pour plus de détail sur le nombre de cycles utilisés par les instructions VTBL et VTBX cliquez ici.

Cas concret

On n’a pas tous les jours l’occasion d’utiliser ce type d’instruction, pourtant dans certain cas, et malgré sa faible performance, elle peut s’avérer bien utile.

Réduction bilinéaire sur l’axe des X d’une image 8 bit.

J’ai été confronté au besoin de réduire une image 8 bit (en niveau de gris quoi !).
Dans ce cas, il est très compliqué pour NEON d’aller lire 3 octets (par exemple) pour les ranger dans 3 registres différents. On pourrait imaginer utiliser l’instruction VLD3, mais cela n’est pas possible car les pixels source que l’on veut charger ne sont pas consécutifs.
Pour la même raison, l’instruction VTRN n’est pas utilisable.

Par exemple, supposons que je veuille exécuter ce code

(P0 * 137 + P1 * 119 + P2 * 0) / 256
(P1 * 18 + P2 * 137 + P3 * 101) / 256
(P3 * 35 + P4 * 137 + P5 * 84) / 256
(P5 * 52 + P6 * 137 + P7 * 67) / 256
(P7 * 69 + P8 * 137 + P9 * 50) / 256
(P9 * 86 + P10 * 137 + P11 * 33) / 256
(P11 * 103 + P12 * 137 + P13 * 16) / 256
(P13 * 120 + P14 * 136 + P15 * 0) / 256

où Px est le numéro du pixel source.

Rem : ce code consiste a réduire 15 pixels source en 8 pixels destination.

Nous souhaitons charger les 15 premiers pixels (en fait on va en charger 16) et les ranger dans 3 registres de la manière suivante

d0 :	p0		p1		p3		p5		p7		p9		p11	p13
d1 :	p1		p2		p4		p6		p8		p10	p12	p14
d2 :	p2		p3		p5		p7		p9		p11	p13	p15

…afin de pouvoir ensuite facilement les multiplier par les coefficients (coefficients qui devront eux-même devoir se trouver dans d’autres registres)

Si les coefficients vont pouvoir être précalculés puis chargés efficacement dans des registres, il n’est par contre, pas possible de charger facilement les pixels afin de les ranger dans les registre d0, d1 et d2 comme nous le désirons.

Nous allons donc dans ce cas utiliser l’instruction VTBL.

Nous allons dans un premier temps, charger dans d7, d8, d9 les indices des pixels que nous voulons réorganiser. La valeur de ces indices, tout comme celle des coefficients vont pouvoir être précalculés.
Cela donne donc le simple code suivant

	vld1.u8			{d7, d8, d9}, [r0]!						@ lecture des indices de pixels
	vld1.u8			{d4, d5, d6}, [r0]!						@ lecture des coefficients

Ce qui nous permet de charger les données suivantes dans les registres

d4 :	137	18		35		52		69		86		103	120
d5 :	119	137	137	137	137	137	137	136
d6 : 	0		101	84		67		50		33		16		0
d7 :	0		1		3		5		7		9		11		13
d8 :	1		2		4		6		8		10		12		14
d9 :	2		3		5		7		9		11		13		15

Puis nous chargeons 16 pixels et nous les réorganisons grâce à des instruction VTBL

	vld1.u8			{d10, d11}, [r1]!							@ lecture de 16 pixels
	vtbl.u8			d0, {d10, d11}, d7
	vtbl.u8			d1, {d10, d11}, d8
	vtbl.u8			d2, {d10, d11}, d9

Et voilà le travail ! On vient de charger 16 pixels dans d10 et d11 puis on est allé chercher dans ces deux registres les pixels (octets) pour les organiser dans d0, d1 et d2 afin d’avoir exactement les pixels dans l’ordre que nous voulions.

Il ne nous reste alors plus que la partie le plus simple de l’interpolation à réaliser.
les multiplications, les additions et pour finir la réduction sur 8 bits

	vmull.u8			q5, d0, d4
	vmlal.u8			q5, d1, d5
	vmlal.u8			q5, d2, d6
	vqrshrn.u16		d12, q5, #8                         @ reduce to 8 bit

Au final d12 contient les 8 pixels résultant des opérations que nous souhaitions réaliser, a savoir réduire 15 pixels source en 8 pixels destination en utilisant la réduction bilinéaire.

Conclusion

L’instruction VTBL, bien que relativement lente (même si je n’y connais rien en micro-câblage, il me semble qu’on parle juste ici de déplacer des bits !) peut s’avérer dans certain cas extrêmement utile.
Son emploi et pourtant limité à la manipulation d’octets (et généralement d’image 8 bits) car pour organiser les valeur 32 bits dans des registres NEON il y a des méthodes plus efficace, comme charger les données avec l’ARM puis les pousser dans les registres NEON en utilisant les registres 32bits de clui-ci (S0 à S31).

Le rêve serait qu’ARM améliore ce type d’instruction pour permettre l’utilisation de registre Qn ou même de liste de registres Dn.

VTBL.8 Qd, {Qlist}, Qm
VTBX.8 {Ddlist}, {Dlist}, {Dmlist}

Nous aurions alors pu dans le cas présenté ci-dessus exécuter les 3 VTBLs en une seule instruction.

 | Tags: , ,

9 Responses to “Neon Instruction : VTBL, VTBX”

  1. XilasZ dit :

    Salut, super article pour aider a dompter neon (même si j’ai pas encore eu à utiliser VTB).

    Je sais pas trop ou le faire donc j’en parle ici, je cherche à optimiser ceci http://pulsar.webshaker.net/ccc/sample-bd263b82 .
    Je constate un comportement étrange du compteur de cycle : au bout d’un moment, les instructions neon sont flaggué en “a.” plutôt que “n.”. De plus les instructions vmov/movsgt ne sont pas reconnu.
    Sinon, j’ai du mal à interpréter le résultat, j’ai l’impression que les oranges ca veut dire qu’il attend d’avoir de la place dans le pipeline et rouge c’est a cause des bulles ? j’ai cru comprendre qu’avant il y avait plus de détails sur le comptage ?

    Je pense qu’une petite doc expliquant un peu, accessible sur la page du compteur serait un plus, avec eventuellement de petits exemple de cas simples. car actuellement il y a des bouts d’infos dans differents articles, ce n’est pas evident.

  2. Etienne SOBOLE dit :

    Ouaip.
    Bon il devient de plus en plus urgent que je mette en ligne une version plus récente du compteur de cycle ;)
    J’ai pu constater le bug du n. qui devient a.
    j’ai aussi vu que l’instruction vmov.u8 ne fonctionne pas !
    vmov.i32 fonctionne par contre… byzarre.

    Donc je vais mettre en ligne prochainement une nouvelle version. C’est juste qu’il faut que je trouve le temps de le faire !

  3. XilasZ dit :

    Ok, merci du retour rapide. Je vais attendre la prochaine version alors pour demander le pourquoi du comment il trouve 9 bulles dès la 2e instruction :p

  4. Etienne SOBOLE dit :

    Pour les codes couleurs:

    - orange ça veut dire que l’instruction est obligé d’attendre le bon pipeline. par exemple MUL ne peut s’exécuter que dans le pipeline 0, donc si tu as 2 mul qui se suivent, bien qu’il y ai une place dans le pipeline 1, le mul va devoir attendre le pipeline 0.

    - rouge ça veut dire que tu pourrais insérer des instructions avant (note que dans le cas du orange ça veut dire que tu peux en insérer une avant). Et c’est là où ça se complique, c’est que c’est pas le nombre de cycle perdu mais le nombre de bulles dans tous les pipelines. Donc dans ton exemple, à un moment tu as un “q15:9 vqadd.s16 Q9, Q15, Q6″ cela veut dire qu’en fait tu perds 4.5 cycles ou plus exactement qu’il y a la place pour mettre avant 9 instructions (pour peu bien sur que tu arrives à pipeliner de façon optimale ce qui est rarement le cas).

    A partir du Cortex A9, NEON n’a plus la possibilité d’exécuter 2 instructions par cycle (possibilité assez réduite mais existante dans le Cortex A8), du coup, la lisibilité du compteur de cycle est plus simple puisque, ton exemple donnerai sans doute “q15:4″, et là ça coïncide avec le nombre de cycles perdus !

    Le principal changement de la prochaine version est là ! Une simplification du compteur de cycles due à la simplification de NEON.

    Par contre, pas de miracle, le compteur de cycle ne sera pas capable de gérer le ré ordonnancement dynamique des instructions du Cortex A9.
    En fait ce sera une version hybride entre le Cortex A8 et le Cortex A9…

  5. XilasZ dit :

    ok, donc le chiffre quand c’est rouge, c’est la somme des bulles des 2 pipelines ?

    Je bloque un peu sur les oranges. Dans le cas d’une sucession d’instructions de un cycle, qui ne partagent aucun registre (comme la : http://pulsar.webshaker.net/ccc/sample-68f7a00b), la première instruction rentre direct dans le pipeline, mais la 2e doit pouvoir rentrer dans le 1er etage du pipeline a l’instruction suivante, et donc ya pas vraiment d’attente dans ce cas ?

  6. Etienne SOBOLE dit :

    C’est pas aussi simple ;)
    Comme expliqué ici

    Pour exécuter 2 instructions dans le même cycle sur NEON :
    - L’une des instructions doit être un accès mémoire ou une permutation.
    - L’autre instruction doit être une opération arithmétique.

    … et encore je ne suis pas sur que ça fonctionne à tous les coups d’après les tests que j’avais fait !

    Et tous cas, 2 vadd ne peuvent pas s’exécuter en même temps.
    par contre ce code là http://pulsar.webshaker.net/ccc/sample-d2794736
    va bien exécuter les deux instructions dans la même cycle.

  7. XilasZ dit :

    J’ai toujours pas bien pigé les oranges de http://pulsar.webshaker.net/ccc/sample-68f7a00b.

    les vadd :
    - peuvent aller uniquement dans le pipeline 0
    - prennent 1 cycle
    - ne partagent aucun registre

    Donc elle se suivent sans trou, et ca va aussi vite que possible, c’est optimal (enfin d’après ce que j’ai compris).

    Dans ce cas, pourquoi de l’orange ? y a-t-il une subtilité qui m’échappe ?

  8. Etienne SOBOLE dit :

    Oui c’est optimal (dans le sens où tu ne peux pas mieux optimiser ce code),
    mais ça ne veut pas dire que tu utilises NEON au mieux de ses capacités!

    NEON à la possibilité de traiter une autre instruction dans le pipeline 1 en même temps qu’un vadd. Raison pour laquelle la ligne est orange.
    Orange veut dire, pas de cycle perdu, mais attente d’un pipeline particulier.

    Ton code prend 5 cycles.
    mais celui-la aussi http://pulsar.webshaker.net/ccc/sample-ba79dbba
    prend 5 cycles et pourtant il traite 5 instructions de plus que le tiens.

    Celui-ci n’a pas de ligne orange car il sature les 2 pipeline de NEON.

    Ceci dit, ça reste théorique et si tu arrives déjà a virer les lignes rouges c’est déjà très bien.
    Il faut savoir sur le Cortex A9 ne peux plus exécuter deux instructions NEON par cycle. Donc optimiser les lignes orange de NEON n’a d’intérêt que pour le Cortex A8 !!!

  9. XilasZ dit :

    Ok, je comprends mieux, merci pour les explications.

    As-tu eu l’occasion d’avancer sur une nouvelle version du compteur ?

Répondre

Human control : 7 + 2 =