Feuille de notes

Feuille de notes

Exercice d'apprentissage

Introduction

SIMD : masques et conditions

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.

Comparaisons empaquetées

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 :

  • Les comparaisons d'entiers reçoivent la condition sous forme de suffixe : 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.
  • Les comparaisons de nombres à virgule flottante reçoivent la condition encodée dans un immédiat. Différentes valeurs de cet immédiat correspondent à différentes conditions testées.

À 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 :

  • Pour les entiers, p + cmp + condition + taille (b, w, d ou q).
  • Pour les flottants, cmp + p + taille (s ou d). La condition est passée dans un immédiat en tant qu'opérande supplémentaire.
Comparaisons d'entiers

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é.

Note

Deux idiomes pratiques :

  1. Faire un XOR d'un registre avec lui-même pour obtenir une valeur tout en zéros.
  2. Comparer un registre avec lui-même pour obtenir une valeur tout en uns.

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.

Comparaisons de nombres à virgule flottante

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

Sélectionner avec un masque

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.

Instructions blend dédiées

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.

Note

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é.

Revenir du masque au scalaire

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.

Tester un vecteur entier

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 :

  1. Utiliser 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.
  2. Utiliser 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.

Instructions

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.

Note

Les calculs de cet exercice doivent être effectués à l'aide d'instructions SIMD.

1. Signale les notes au-dessus du seuil

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.

2. Signale les notes parfaites

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.

3. Attribue un rang

Chaque note obtient un rang de 1 à 3 :

  • Rang 1 pour une note inférieure ou égale au seuil de réussite de 50.0.
  • Rang 2 pour une note au-dessus de ce seuil, mais en dessous du maximum.
  • Rang 3 pour une note parfaite, égale au maximum.

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.

4. Compte les échecs

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.

5. Tout le monde a-t-il réussi ?

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.

Modifie via GitHub Le lien s'ouvre dans une nouvelle fenêtre ou un nouvel onglet
x86-64 Assembly Exercism

Prêt à commencer Feuille de notes ?

Inscris-toi sur Exercism pour apprendre et maîtriser x86-64 Assembly avec 22 concepts130 exercices, et un vrai mentorat humain, le tout gratuitement.