Un premier exemple¶
Pour manipuler en C, les registres introduits depuis le jeu d’instructions SSE, il existe différents types disponibles :
- __m128i, __m256i, __m512i où chaque composante d’un vecteur est considérée comme un entier,
- __m128, __m256, __m512 où chaque composante d’un vecteur est considérée comme un réel simple précision,
- __m128d, __m256d, __m512d où chaque composante d’un vecteur est considérée comme un réel double précision.
Le guide de référence détaillant l’ensemble des intsructions disponibles pour manipuler ces registres est disponibles à l’adresse https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html.
Pour la suite, je ne considèrerai que les vecteurs à coordonnées entières positives, simplement pour faciliter l’affichage final des résultats, ce n’est qu’un exemple introductif. De même, pour ce premier exemple, on se concentrera sur la manipulation des registres 128 bits. Un tel registre peut être utilisé pour stocker par exemple 4 coordonnées codées chacune sur 32 bits. Réalisons un programme qui effectue la somme de 2 vecteurs de 4 composantes et affiche le résultat.
Le code source C permettant de manipuler ces registres doit inclure l’entête suivante:
#include <immintrin.h>
On déclare ensuite 3 variables de 128 bits:
__m128i V1, V2, S;
Il existe plusieurs façons pour initialiser un registre :
- via une instruction explicite permettant de spécifier la valeur de chaque composante _mm_set_epiX où X=8, 16, 32 ou 64,
- via une instruction permettant d’indiquer une adresse mémoire pointant sur la suite des composantes à charger dans le registre _mm_loadu_siX où X=16, 32 ou 64 (le chargement à partir d’une adresse pointant sur des octets n’est pas prévu).
Nous utiliserons ici le chargement explicite.
// création du vecteur (0,1,2,3) = (x0,x1,x2,x3)
V1 = _mm_set_epi32(3,2,1,0); // du mot de poids fort vers le mot de poids faible
// création du vecteur (10,20,30,40)
V2 = _mm_set_epi32(40,30,20,10);
L’addition des 2 vecteurs (et donc les 4 additions simultanées d’objets de 32 bits) se fait en utilisant l’instruction _mm_add_epi32 :
S = _mm_add_epi32(V1,V2);
Il n’existe pas d’instruction pour afficher le contenu d’un registre. Une solution possible est d’utiliser un pointeur pour parcourir le registre :
(uint32_t *)somme = (uint32_t *)&S;
printf("(");
for (int i = 0; i < 3; i++)
printf("%d,",somme[i]);
printf("%d)\n",somme[3]);
Code source complet :
#include <immintrin.h>
#include <stdint.h>
#include <stdio.h>
int main(void)
{
__m128i V1, V2, S;
// création du vecteur (0,1,2,3) = (x0,x1,x2,x3)
V1 = _mm_set_epi32(3,2,1,0); // du mot de poids fort vers le mot de poids faible
// création du vecteur (10,20,30,40)
V2 = _mm_set_epi32(40,30,20,10);
// Calcul du résultat
S = _mm_add_epi32(V1,V2);
uint32_t *somme = (uint32_t *)&S;
printf("(");
for (int i = 0; i < 3; i++)
printf("%d,",somme[i]);
printf("%d)\n",somme[3]);
}
La compilation du code se fait en utilisant la directive -march=native de gcc qui indique de prendre en compte les extensions disponibles sur le processeur.
% gcc -march=native add_vec128.c -o add_vec128
% ./add_vec128
(10,21,32,43)
Les extensions disponibles sur un processeur sont accessibles via le fichier /proc/cpuinfo. Ci-dessous un exemple pour une machine intégrant les extensions SSE, AVX et AVX2.
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2
ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf
pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3
sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2
x2apic movbe popcnt
tsc_deadline_timer aes xsave avx
f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp
tpr_shadow vnmi flexpriority ept vpid ept_ad fsgsbase tsc_adjust bmi1 hle avx2
smep bmi2 erms invpcid rtm mpx rdseed adx smap
clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp md_clear flush_l1d