Chaque opération SIMD jusqu'ici a agi voie par voie.
La valeur de la voie i du résultat est calculée sur la voie i des entrées.
Cependant, il existe de nombreuses situations où déplacer des valeurs entre voies est nécessaire ou souhaitable. Par exemple, les voies peuvent être dans le désordre pour le calcul que l'on doit effectuer.
Il existe de nombreuses instructions SIMD qui déplacent des données entre voies de différentes manières.
Un mélange réordonne les octets ou les voies d'un registre, en tirant chaque voie de destination depuis une voie source qui peut se trouver n'importe où. Ils peuvent sélectionner la même voie pour différentes destinations, ce qui leur permet également de diffuser des valeurs.
Les instructions de mélange se comportent différemment selon leur taille et leur domaine d'exécution.
L'instruction pshufd réorganise les quatre voies de 32 bits de sa source dans sa destination.
La sélection est un immédiat de 8 bits, lu comme quatre champs de 2 bits, un par voie de destination. Chaque champ sélectionne laquelle des quatre voies source copier dans cette voie de destination :
| champ | indice de voie |
|---|---|
00 |
0 |
01 |
1 |
10 |
2 |
11 |
3 |
La position du champ dans l'immédiat, lue de droite à gauche, indique où la voie sélectionnée est insérée. Une voie source peut être sélectionnée plus d'une fois, ce qui permet de diffuser une seule voie sur tout le registre :
; The bit fields are read right-to-left
pshufd xmm1, xmm0, 0b00_01_10_11 ; reverse: lane 0 takes source 0b11 (3), lane 1 takes source 0b10 (2), and so on
pshufd xmm3, xmm2, 0b00_00_00_00 ; broadcast source lane 0 into all four lanes
Il existe deux instructions de mélange pour les nombres à virgule flottante, qui suivent la même syntaxe générale : shuf + p + le suffixe de taille (s ou d).
Elles utilisent également un immédiat pour sélectionner les voies.
shufps utilise le même encodage de champ de 2 bits que les mélanges entiers précédents.
Cependant, comme il n'y a que 2 voies de 64 bits dans un opérande de 128 bits, shufpd n'utilise qu'un encodage de champ de 1 bit.
Ces instructions diffèrent de leurs homologues entières en ce qu'elles tirent les voies de deux opérandes plutôt que d'un seul :
shufps xmm0, xmm1, 0b11_10_01_00 ; xmm0 = {xmm0[0], xmm0[1], xmm1[2], xmm1[3]}
shufps xmm2, xmm3, 0b00_00_00_00 ; xmm2 = {xmm2[0], xmm2[0], xmm3[0], xmm3[0]}
shufpd xmm4, xmm5, 0b0_1 ; xmm4 = {xmm4[1], xmm5[0]}
Comme les deux opérandes alimentent le résultat, elles peuvent être utilisées pour entrelacer deux vecteurs en une seule étape. Si la source et la destination sont le même registre, chaque voie est tirée de celui-ci.
pshufb est le mélange le plus général.
Bien que pshufb et pshufd aient des noms très similaires, ne différant que par le suffixe de taille, ils effectuent des opérations très différentes.
Premièrement, alors que pshufd utilise un immédiat pour sélectionner les positions des voies, pshufb utilise un vecteur de contrôle dans l'opérande source.
Ce vecteur de contrôle est un registre xmm ou un opérande mémoire de 16 octets.
Pour chacune des 16 voies de destination, les quatre bits de poids faible du vecteur de contrôle donnent un indice d'octet source, de 0 à 15.
Si la voie i du vecteur de contrôle contient l'indice d'octet source j, alors la j-th-ième voie de la destination sera déplacée vers la i-ième voie.
Cela met en évidence une seconde différence entre les deux instructions.
Alors que pshufd prend les voies de mélange dans un opérande source différent, pshufb effectue le mélange sur place dans la destination.
section .rodata
align 16
reverse: db 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0
section .text
fn:
pshufb xmm0, [rel reverse]
; xmm0[0] = xmm0[reverse[0]] (xmm0[15])
; xmm0[1] = xmm0[reverse[1]] (xmm0[14])
; ...
; in the end, the bytes in xmm0 are reversed
pshufb est encore plus flexible que cela : il peut effacer n'importe laquelle des voies.
Si le bit de poids fort est mis à 1 dans une voie i du vecteur de contrôle, alors la voie i-th de la destination est mise à zéro.
Cela fait de pshufb à la fois une permutation d'octets arbitraire et un effacement sélectif, en une seule instruction.
Comme pshufb agit à une granularité d'octet, il peut aussi être utilisé pour mélanger des voies de tailles différentes, en regroupant des voies voisines.
Par exemple, il peut être utilisé pour mélanger des dwords :
section .rodata
align 16
input: dd 13, 25, 37, 49 ; each number takes 4 bytes
reverse_dwords: db 12, 13, 14, 15, 8, 9, 10, 11, 4, 5, 6, 7, 0, 1, 2, 3 ; aligned too, since input spans 16 bytes
section .text
fn:
movdqa xmm0, [rel input] ; xmm0 = {13, 25, 37, 49}
pshufb xmm0, [rel reverse_dwords]
; xmm0 = {49, 37, 25, 13}
Les instructions unpack prennent des voies de deux opérandes et les tissent ensemble, en alternant entre les deux. Il existe des variantes entières et flottantes et elles suivent largement la même structure syntaxique générale, avec deux différences :
l ou h pour indiquer s'il agit sur la moitié basse (l) ou la moitié haute (h) de chaque opérande.bw ou qdq (le suffixe de taille pour 16 octets est dq, comme dans movdqu).punpcklwd xmm0, xmm1 ; p + unpck + l + wd
; this interleaves the first 4 words of xmm0 and xmm1 into 4 dwords
; for each dword in the result, the first word is taken from xmm0 and the second, from xmm1
; xmm0 = {xmm0[0], xmm1[0], xmm0[1], xmm1[1], xmm0[2], xmm1[2], xmm0[3], xmm1[3]}
unpckhps xmm2, xmm3 ; unpck + h + p + s
; this interleaves the high 32-bit floats of xmm2 and xmm3
; xmm2 = {xmm2[2], xmm3[2], xmm2[3], xmm3[3]}
Les instructions pack vont dans l'autre sens, en combinant les voies de deux opérandes en voies de demi-largeur de la destination.
Ces instructions ne prennent pas de préfixe p.
À part cela, la syntaxe combine des éléments que l'on a déjà vus :
pack.s ou un u pour indiquer si les valeurs de sortie sont signées ou non signées, respectivement.s pour saturant, comme dans l'arithmétique saturante vue dans un concept précédent.unpck).Comme l'opération pack est un rétrécissement, elle n'a pas besoin d'un suffixe l ou h :
packssdw xmm0, xmm1 ; pack + s (for signed) + s (for saturating) + dw (dword to word)
packuswb xmm2, xmm3 ; pack + u (for unsigned) + s (for saturating) + wb (word to byte)
Ces instructions sont saturantes, c'est-à-dire que les valeurs de sortie sont saturées à la plage plus étroite. Elles opèrent de double mot à mot et de mot à octet. Il n'y a pas de variante de quadruple mot à double mot.
Note que l'entrée est toujours interprétée comme signée.
Le type, signé ou non signé, concerne la sortie.
Il indique la plage à saturer.
Par exemple, packsswb sature à la plage d'un octet signé, c'est-à-dire [-128, 127].
Les voies basses du résultat proviennent de l'opérande de destination et les voies hautes de la source :
packusdw xmm0, xmm1 ; 8 words, each clamped to 0..65535
; xmm0 = {xmm0[0], xmm0[1], xmm0[2], xmm0[3], xmm1[0], xmm1[1], xmm1[2], xmm1[3]}
Il n'existe pas d'équivalent à virgule flottante pour ces instructions.
Jusqu'à présent, on a toujours déplacé des données entre un registre SIMD et un registre à usage général en utilisant movq/movd.
Ces instructions ne peuvent écrire ou lire que dans la voie basse d'un registre SIMD, et, lors de l'écriture, elles effacent toutes les autres voies.
Il existe des instructions qui font la même chose pour n'importe quelle voie, pas seulement la première, en laissant les autres voies intactes :
Les deux suivent la syntaxe entière : p + insr/extr + le suffixe de taille (b, w, d ou q).
Dans les deux cas, le registre à usage général est généralement de 32 bits.
Seuls pinsrq et pextrq nécessitent un opérande de 64 bits.
Un opérande mémoire a toujours la taille de l'opération : 8 bits pour pinsrb/pextrb, 16 bits pour pinsrw/pextrw, etc.
pinsrb xmm0, eax, 5 ; replace byte 5 of xmm0 with the low byte of eax
pextrb byte [rdx], xmm0, 5 ; copy byte 5 of xmm0 into the memory location indicated by rdx
Note que ces instructions, tout comme movd et movq, déplacent simplement des octets bruts.
Cela signifie qu'elles peuvent aussi être utilisées pour des valeurs à virgule flottante stockées en mémoire ou dans un registre à usage général.
Tu écris les boucles internes d'un pipeline logiciel de traitement d'image, l'étape qui prépare les textures et compose les calques avant qu'ils n'atteignent l'écran. Le pipeline travaille sur les pixels un bloc à la fois, en appliquant la même opération à tout le bloc.
Un pixel est composé de quatre canaux d'un octet : rouge, vert, bleu et alpha, dans cet ordre (RGBA).
Un bloc fait 4 pixels, soit 16 octets au total.
Tu as cinq tâches.
Les opérandes te sont fournis sous forme d'adresses mémoire, et tu écris ta réponse à une adresse de résultat. 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, et non d'opérations scalaires.
Les textures sont stockées en RGBA, mais le framebuffer dans lequel dessine ce pipeline attend chaque pixel en ordre BGRA : les canaux rouge et bleu échangés, les canaux vert et alpha laissés en place.
Une image arrive sous la forme d'une séquence de blocs, et chaque bloc est converti de la même manière.
Implémente la fonction to_display_order, qui convertit une image entière de RGBA en BGRA, un bloc à la fois.
Tu dois définir le masque de contrôle qui réordonne les canaux comme une constante vectorielle en mémoire, et le réutiliser pour chaque bloc.
Cette fonction prend comme arguments, dans cet ordre :
result : adresse mémoire d'un tampon dans lequel les blocs convertis sont écrits, 16 octets par bloc.pixels : adresse mémoire des blocs source, 4 pixels par bloc, chaque pixel occupant 4 octets dans l'ordre RGBA.block_count : le nombre de blocs, toujours supérieur à 0.pixels = {200, 64, 32, 255, 10, 20, 30, 40, 0, 0, 0, 255, 12, 34, 56, 78,
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} // 2 blocks
block_count = 2
result = {32, 64, 200, 255, 30, 20, 10, 40, 0, 0, 0, 255, 56, 34, 12, 78,
3, 2, 1, 4, 7, 6, 5, 8, 11, 10, 9, 12, 15, 14, 13, 16}
Cette fonction n'a pas de valeur de retour.
Pour effacer une zone ou peindre une plage uniforme, le pipeline écrit une seule couleur sur chaque pixel de la zone.
Implémente la fonction fill_region, qui remplit une zone de block_count blocs avec des copies d'une même couleur.
Cette fonction prend comme arguments, dans cet ordre :
result : adresse mémoire d'un tampon dans lequel les blocs remplis sont écrits, 16 octets par bloc.color : adresse mémoire d'un pixel, 4 octets dans l'ordre RGBA.block_count : le nombre de blocs à remplir, toujours supérieur à 0.color = {18, 52, 86, 120}
block_count = 2
result = {18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86, 120,
18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86, 120}
Cette fonction n'a pas de valeur de retour.
Deux calques à un seul canal doivent être fusionnés dans un même tampon, avec leurs échantillons entrelacés. Le résultat alterne un échantillon du premier calque, puis un du second.
Implémente la fonction weave_scanlines, qui entrelace deux lignes de 16 échantillons chacune en une seule ligne de 32 échantillons.
Cette fonction prend comme arguments, dans cet ordre :
result : adresse mémoire d'un tampon dans lequel les 32 échantillons entrelacés sont écrits.first : adresse mémoire de la première ligne, 16 échantillons, chacun une valeur de 8 bits.second : adresse mémoire de la deuxième ligne, 16 échantillons, chacun une valeur de 8 bits.first = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
second = {100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115}
result = {0, 100, 1, 101, 2, 102, 3, 103, 4, 104, 5, 105, 6, 106, 7, 107,
8, 108, 9, 109, 10, 110, 11, 111, 12, 112, 13, 113, 14, 114, 15, 115}
Cette fonction n'a pas de valeur de retour.
Une passe de luminosité met chaque échantillon à l'échelle en précision de calcul de 16 bits, de sorte qu'un échantillon trop lumineux peut dépasser 255 et qu'une différence peut descendre en dessous de 0.
L'étape finale ramène ces valeurs de calcul à des échantillons de 8 bits pour l'affichage, en bornant à 0 toute valeur inférieure à 0 et à 255 toute valeur supérieure à 255.
Implémente la fonction pack_samples, qui ramène deux groupes de 8 valeurs de calcul à une seule ligne de 16 échantillons, dans l'ordre.
Cette fonction prend comme arguments, dans cet ordre :
result : adresse mémoire d'un tampon dans lequel les 16 échantillons bornés sont écrits, chacun une valeur de 8 bits.first : adresse mémoire des 8 premières valeurs de calcul, chacune un entier signé de 16 bits.second : adresse mémoire des 8 valeurs de calcul suivantes, chacune un entier signé de 16 bits.first = {300, -5, 128, 255, 0, 400, 64, 200}
second = {255, 256, -1, 100, 50, 1000, 7, 0}
result = {255, 0, 128, 255, 0, 255, 64, 200, 255, 255, 0, 100, 50, 255, 7, 0}
Cette fonction n'a pas de valeur de retour.
Les données de texture et de sommets arrivent souvent entrelacées, les x et y de chaque point étant regroupés.
Cependant, il est souvent nécessaire de les séparer pour traiter les données efficacement : toutes les valeurs x dans un vecteur, toutes les valeurs y dans un autre.
Implémente la fonction split_coordinates, qui sépare quatre points (x, y) entrelacés en un vecteur de coordonnées x et un vecteur de coordonnées y.
Cette fonction prend comme arguments, dans cet ordre :
xs : adresse mémoire d'un tampon dans lequel les 4 coordonnées x sont écrites, 4 nombres à virgule flottante de 32 bits.ys : adresse mémoire d'un tampon dans lequel les 4 coordonnées y sont écrites, 4 nombres à virgule flottante de 32 bits.first : adresse mémoire des deux premiers points, 4 nombres à virgule flottante de 32 bits, sous la forme {x0, y0, x1, y1}.second : adresse mémoire des deux points suivants, 4 nombres à virgule flottante de 32 bits, sous la forme {x2, y2, x3, y3}.first = {0.0, 0.5, 1.0, 1.5} // {x0, y0, x1, y1}
second = {2.0, 2.5, 3.0, 3.5} // {x2, y2, x3, y3}
xs = {0.0, 1.0, 2.0, 3.0} // {x0, x1, x2, x3}
ys = {0.5, 1.5, 2.5, 3.5} // {y0, y1, y2, y3}
Cette fonction n'a pas de valeur de retour.
Inscris-toi sur Exercism pour apprendre et maîtriser x86-64 Assembly avec 22 concepts130 exercices, et un vrai mentorat humain, le tout gratuitement.