Le code scalaire s'appuie sur des drapeaux positionnés par diverses instructions pour effectuer un branchement en réponse à une certaine condition. Les valeurs empaquetées, quant à elles, ne représentent pas une seule valeur mais plusieurs, en parallèle. Une même condition peut être fausse pour une voie et vraie pour une autre.
C'est pourquoi le code SIMD est branchless par défaut.
Au lieu de s'appuyer sur des drapeaux, les comparaisons empaquetées produisent généralement un masque dans l'opérande de destination.
Pour chaque voie, la comparaison remplit la voie entière de uns quand la condition est vraie, et de zéros quand elle est fausse.
Lue comme un entier signé, une voie vraie vaut -1 et une voie fausse vaut 0.
Ce masque peut ensuite être combiné avec des opérations bit à bit pour filtrer des voies précises.
Un cmp scalaire est générique en ce sens qu'il sert à positionner plusieurs drapeaux à la fois.
Une autre instruction peut ensuite utiliser ces drapeaux pour effectuer un branchement ou des calculs.
Cependant, comme une comparaison empaquetée vérifie une condition et calcule un masque à la fois, elle n'est pas générique. Il faut indiquer à la comparaison la condition exacte testée.
Il y a deux façons de procéder :
eq pour l'égalité, et gt pour « supérieur à ».
Les autres variantes se construisent en combinant le résultat de l'une de ces comparaisons.À part l'emploi d'un suffixe conditionnel spécifique sur les comparaisons d'entiers, la syntaxe suit la même structure que celle que l'on a déjà vue :
p + cmp + condition + taille (b, w, d ou q).cmp + p + taille (s ou d).
La condition est passée dans un immédiat en tant qu'opérande supplémentaire.Comme mentionné, il n'existe que des comparaisons d'entiers pour l'égalité et « supérieur à » :
| instruction | description |
|---|---|
pcmpeqb, pcmpeqw, pcmpeqd, pcmpeqq
|
égalité par voie |
pcmpgtb, pcmpgtw, pcmpgtd, pcmpgtq
|
« supérieur à » signé par voie |
movdqa xmm0, [rel scores]
pcmpgtd xmm0, [rel threshold] ; lane i = 0xFFFFFFFF (-1) if scores[i] > threshold[i], else 0
Pour créer une comparaison « inférieur à », utilise gt avec les opérandes inversés : a < b == b > a.
Note que la comparaison est signée. Pour effectuer une comparaison non signée, inverse le bit de poids fort des deux opérandes. On peut le faire avec un XOR avec un masque où seul le bit de poids fort est positionné.
Deux idiomes pratiques :
Par exemple :
pxor xmm4, xmm4 ; xmm4 = all zeros
pcmpeqd xmm7, xmm7 ; xmm7 = all ones
Les valeurs tout en zéros et tout en uns sont des masques courants pour encoder respectivement « faux partout » et « vrai partout ».
On peut aussi les utiliser pour représenter un 0 empaqueté ou un -1 empaqueté, qui sont des valeurs sentinelles courantes.
Par exemple, le NUL qui marque la fin d'une string est un 0.
Les voies flottantes utilisent une forme différente : une seule instruction, cmpps (et cmppd pour les voies 64 bits), avec la condition sous forme d'immédiat :
movaps xmm0, [rel readings]
cmpps xmm0, [rel limits], 1 ; condition 1 is "less than": lane i = all ones if readings[i] < limits[i]
NASM dispose aussi de pseudo-op qui correspondent au bon immédiat et sont plus faciles à retenir.
Dans tout ce qui suit, x dans px peut être s (flottants 32 bits) ou d (flottants 64 bits) :
| pseudo-op | immédiat | comparaison |
|---|---|---|
cmpeqpx |
0 | a == b |
cmpltpx |
1 | a < b |
cmplepx |
2 | a <= b |
cmpunordpx |
3 | a est NaN ou b est NaN |
cmpneqpx |
4 | a != b |
cmpnltpx |
5 | a >= b |
cmpnlepx |
6 | a > b |
cmpordpx |
7 | ni a ni b n'est NaN |
Un masque encode le résultat d'une condition. On peut ensuite l'utiliser pour choisir, voie par voie, entre deux ensembles de valeurs selon cette condition. On prend la voie d'une valeur là où le masque est vrai, et d'une autre là où il est faux :
; result = (a AND mask) OR (b AND NOT mask)
movdqa xmm2, xmm0 ; xmm0 holds the mask, keep a copy
pand xmm2, xmm3 ; xmm2 = a AND mask: lanes of a where mask is true
pandn xmm0, xmm4 ; xmm0 = NOT mask AND b: lanes of b where mask is false
por xmm2, xmm0 ; combine the two halves
Note que l'asymétrie de pandn est payante ici : le masque se trouve dans la destination, est nié, et sélectionne depuis b en une seule instruction.
Ce schéma est la forme empaquetée de la sélection sans branchement.
Chaque voie est calculée, et le masque seul décide quelle valeur survit, sans aucun jcc.
Il existe des instructions qui effectuent directement cette même sélection, en lisant un bit par élément dans un registre de masque. On les appelle les instructions blend :
| instruction | élément | source du masque |
|---|---|---|
pblendvb |
octet |
xmm0 implicite |
blendvps |
voie 32 bits |
xmm0 implicite |
blendvpd |
voie 64 bits |
xmm0 implicite |
Note que la première instruction suit la syntaxe des entiers, tandis que les deux autres suivent la syntaxe des flottants. Cependant, comme ces instructions sélectionnent simplement des octets bruts, n'importe laquelle peut servir avec des entiers comme avec des flottants.
Pour chaque élément, le blend conserve la destination lorsque le bit de poids fort de l'élément de masque correspondant est à zéro, et prend la source lorsqu'il est positionné.
Seul ce bit de poids fort est consulté, ce qu'un masque de comparaison satisfait, puisque ses voies sont tout en uns ou tout en zéros.
Le registre de masque est toujours xmm0, qui est implicite :
movaps xmm0, [rel mask] ; the selecting mask must be in xmm0
movaps xmm1, [rel b] ; destination: kept where the mask bit is clear
blendvps xmm1, [rel a] ; source: taken where the mask bit is set
Il est aussi possible d'utiliser pblendvb pour sélectionner des voies à partir d'un masque de comparaison, pour n'importe quelle autre taille.
Comme tous les octets d'une voie vraie sont tout en uns, pblendvb les sélectionne tous.
Ces instructions ajoutent toutes un v après l'opération effectuée (blend).
Ce v signifie variable, car la sélection n'est pas statique : elle dépend d'un registre.
Il existe aussi des variantes sans v, qui sélectionnent selon un immédiat.
Elles suivent le même schéma, en sélectionnant une voie i si le bit i de l'immédiat est positionné.
Bien que puissant, le code SIMD est loin d'avoir la souplesse du code scalaire. Dans de nombreuses situations, il est nécessaire de repasser d'un registre empaqueté au monde des instructions scalaires.
La famille d'instructions movmsk fait le pont entre ces deux mondes.
Ces instructions extraient le bit de poids fort de chaque voie vers un registre à usage général :
| instruction | rassemble | largeur du résultat |
|---|---|---|
pmovmskb |
le bit de poids fort de chacun des 16 octets | 16 bits |
movmskps |
le bit de poids fort de chacun des 4 dwords | 4 bits |
movmskpd |
le bit de poids fort de chacun des 2 qwords | 2 bits |
S'il est utilisé après une comparaison, chaque bit positionné représente une voie « vraie » et chaque bit à zéro, une voie « fausse ».
Ce résultat peut ensuite être manipulé comme d'habitude avec des instructions scalaires.
Par exemple, un popcnt compte le nombre de correspondances, et tzcnt trouve la première.
Le registre à usage général peut faire 32 ou 64 bits de large.
Il existe aussi une variante empaquetée de l'instruction scalaire test : ptest.
Elle est semblable à sa contrepartie scalaire en ce qu'elle effectue une opération AND entre deux opérandes, sans les modifier.
Contrairement à test, ptest effectue aussi une opération ANDN, qui nie le premier opérande.
Ainsi, on peut voir ptest comme une version non destructive de pand et pandn qui positionne des drapeaux selon le résultat.
De la même manière que ces deux instructions, ptest traite tout le registre SIMD comme une seule voie et ne prend donc pas de préfixe de taille.
Si le résultat d'une opération AND vaut 0, le ZF est positionné, et si l'opération ANDN donne 0, c'est le CF qui est positionné.
Cela signifie que ptest peut servir à vérifier à la fois un masque tout en uns et un masque tout en zéros :
ptest sur un registre avec lui-même positionne ZF seulement si le registre est tout en zéros.
Cela imite l'idiome scalaire courant consistant à utiliser test sur un registre avec lui-même pour vérifier qu'il vaut 0.ptest sur un registre avec un masque tout en uns positionne CF seulement si le registre est tout en uns.
De plus, cela positionne ZF seulement si le registre est tout en zéros, ce qui permet de vérifier les deux masques d'un coup.pxor xmm0, xmm0 ; all zeros
pcmpeqb xmm1, xmm1 ; all ones
pcmpeqb xmm2, xmm2
ptest xmm0, xmm0 ; ZF set: a register against itself detects all zeros
ptest xmm0, xmm1 ; ZF set, CF clear: xmm0 is all zeros, not all ones
ptest xmm2, xmm1 ; CF is set only if xmm2 is all ones
Le résultat d'un ptest peut servir à effectuer un branchement ou dans des instructions sans branchement telles que setcc ou cmovcc, comme d'habitude.
Tu gères la station de notation d'une école : tu notes les résultats de la classe bloc par bloc.
Chaque bloc contient 4 résultats, et la station applique la même opération à chaque résultat du bloc. Une note est un nombre à virgule flottante de 32 bits. Plusieurs étapes utilisent un masque : un bloc de 4 voies, chacune valant soit tout à un (un oui pour ce résultat), soit tout à zéro (un non).
Tu as cinq tâches. Tu reçois les opérandes par l'intermédiaire d'adresses mémoire. Certaines tâches écrivent leur réponse à une adresse de résultat, tandis que d'autres la renvoient directement.
Toutes les adresses mémoire de cet exercice sont alignées sur 16 octets.
Les calculs de cet exercice doivent être effectués à l'aide d'instructions SIMD.
La première étape évalue chaque résultat par rapport à un seuil. Un résultat franchit la barre lorsque sa note est strictement supérieure au seuil. Une note inférieure ou égale au seuil ne la franchit pas.
Implémente la fonction flag_above_threshold, qui construit un masque avec une voie tout à un pour chaque note au-dessus de son seuil, et une voie tout à zéro sinon.
Cette fonction prend comme arguments, dans cet ordre :
result : adresse mémoire d'un tampon où sont écrites les 4 voies du masque.scores : adresse mémoire des notes, avec 4 nombres à virgule flottante normaux de 32 bits (jamais NaN).thresholds : adresse mémoire du seuil de chaque voie, avec 4 nombres à virgule flottante normaux de 32 bits (jamais NaN).scores = {72.0, 55.0, 90.0, 40.0}
thresholds = {60.0, 60.0, 60.0, 60.0}
result = {0xFFFFFFFF, 0x00000000, 0xFFFFFFFF, 0x00000000}
Cette fonction n'a pas de valeur de retour.
Un rapport séparé met en évidence les résultats parfaits, ceux qui ont atteint la note maximale possible.
Implémente la fonction flag_perfect, qui construit un masque avec une voie tout à un pour chaque note égale à son maximum, et une voie tout à zéro sinon.
Cette fonction prend comme arguments, dans cet ordre :
result : adresse mémoire d'un tampon où sont écrites les 4 voies du masque.scores : adresse mémoire des notes, avec 4 nombres à virgule flottante normaux de 32 bits (jamais NaN).maxima : adresse mémoire de la note maximale pour chaque voie, avec 4 nombres à virgule flottante normaux de 32 bits (jamais NaN).scores = {100.0, 88.0, 100.0, 73.0}
maxima = {100.0, 100.0, 100.0, 100.0}
result = {0xFFFFFFFF, 0x00000000, 0xFFFFFFFF, 0x00000000}
Cette fonction n'a pas de valeur de retour.
Chaque note obtient un rang de 1 à 3 :
50.0.Implémente la fonction assign_ranks, qui écrit le rang de chaque note.
Tu dois définir le seuil de réussite et les valeurs de rang comme des constantes empaquetées en mémoire. Les fonctions des deux tâches précédentes peuvent être réutilisées : une note est au moins de rang 2 lorsqu'elle est au-dessus du seuil, et de rang 3 lorsqu'elle est égale au maximum.
Cette fonction prend comme arguments, dans cet ordre :
result : adresse mémoire d'un tampon où sont écrits les 4 rangs, chacun un entier non signé de 32 bits.scores : adresse mémoire des notes, avec 4 nombres à virgule flottante normaux de 32 bits (jamais NaN).maxima : adresse mémoire de la note maximale pour chaque voie, avec 4 nombres à virgule flottante normaux de 32 bits (jamais NaN).scores = {40.0, 75.0, 100.0, 60.0}
maxima = {100.0, 100.0, 100.0, 100.0}
result = {1, 2, 3, 2}
Cette fonction n'a pas de valeur de retour.
Au fil de l'année, chaque élève accumule un rang total. La station compte combien de rangs, sur l'ensemble de la cohorte, passent sous un seuil de réussite, afin de planifier le nombre de cours supplémentaires à organiser.
Implémente la fonction count_failures, qui renvoie combien de rangs, dans chaque bloc, sont strictement inférieurs au seuil de réussite.
Le seuil est fourni sous la forme d'un bloc de 4 voies identiques, ce qui te permet de le charger une fois et de le réutiliser pour chaque bloc.
Cette fonction prend comme arguments, dans cet ordre :
ranks : adresse mémoire des rangs, un nombre entier de blocs de 4 voies, chaque rang étant un entier non signé de 32 bits.block_count : le nombre de blocs de 4 voies, toujours supérieur à 0.pass_threshold : adresse mémoire du seuil de réussite, avec 4 entiers identiques de 32 bits.ranks = {1, 2, 3, 1, 2, 2, 1, 3} // 2 blocks
block_count = 2
pass_threshold = {2, 2, 2, 2}
// => 3
Cette fonction renvoie le compte sous forme d'entier signé de 32 bits.
Avant que les dossiers ne soient classés, la station vérifie si la cohorte est propre : elle est validée si aucun résultat n'a échoué dans aucun bloc.
Implémente la fonction all_passed, qui renvoie 1 si tous les élèves ont réussi, et 0 sinon.
Un élève réussit lorsque sa voie correspondante dans le tableau failing est tout à zéro.
Cette fonction prend comme arguments, dans cet ordre :
failing : adresse mémoire des masques d'échec, un nombre entier de blocs de 4 voies, chaque voie valant tout à un ou tout à zéro.block_count : le nombre de blocs de 4 voies, toujours supérieur à 0.failing = {0x00000000, 0x00000000, 0x00000000, 0x00000000,
0x00000000, 0x00000000, 0x00000000, 0x00000000} // 2 blocks
block_count = 2
// => 1
Cette fonction renvoie la réponse sous forme d'entier signé de 32 bits, soit 1, soit 0.
Inscris-toi sur Exercism pour apprendre et maîtriser x86-64 Assembly avec 22 concepts130 exercices, et un vrai mentorat humain, le tout gratuitement.