6. TP 6
Produit Scalaire, suite et fin
Vous pouvez retrouver une vidéo de ce TP sur youtube.
6.1. Programme de test
Pour faire suite au TP 5, on désire écrire un
programme bash (test_dot_product.sh) qui permet de tester
le temps de calcul de chaque méthode et d'afficher un tableau de résultats :
01 dp_ref 6.25 22536539604 1572852
02 dp_fpu 6.25 22517725644 1572852
03 dp_sse_low 3.75 13571628624 1572852
04 dp_sse 0.94 3391219296 1572852
05 dp_avx 0.48 1740976056 1572852
...
On affiche les colonnes suivantes pour un test exécuté 10_000 fois sur des vecteurs
de 524_288 floats :
- le numéro de la méthode sur 2 caractères
- le nom de la méthode sur 22 caractères
- le temps total d'exécution du programme (5 caractères avec 2 chiffres après la virgule)
- le nombre de cycles processeur pour l'exécution de la partie du calcul (20 caractères)
- le résultat obtenu (12 caractères sans chiffre après la virgule)
Les paramètres des tests sont les suivants :
- nombre de répétitions : -z 10000
- longueur des vecteurs : -s 524288
6.2. Méthode utilisant les unités vectorielles SSE et dépliage par 2
Ecrire la méthode dp_sse_vec_ur2 qui réalise le calcul du produit scalaire en
utilisant $4 × 32$ bits d'un vecteur SSE et en dépliant la boucle de calcul par 2.
/**
* Use of SSE vector units
* unroll loop by a factor of 2
*/
f32 dp_sse_vec(f32 *x, f32 *y, u32 size) {
// we use only one vector but twice
f32 v_sum[4];
v_sum[0] = v_sum[1] = v_sum[2] = v_sum[3] = 0.0;
u32 i;
// loop unrolling by a factor of 2x4
for (i = 0; i < (size & ~7); i += 8) {
v_sum[0] += x[i + 0] * y[i + 0];
v_sum[1] += x[i + 1] * y[i + 1];
v_sum[2] += x[i + 2] * y[i + 2];
v_sum[3] += x[i + 3] * y[i + 3];
v_sum[0] += x[i + 4] * y[i + 4];
v_sum[1] += x[i + 5] * y[i + 5];
v_sum[2] += x[i + 6] * y[i + 6];
v_sum[3] += x[i + 7] * y[i + 7];
}
// sum of partial sums
float sum = v_sum[0] + v_sum[1] + v_sum[2] + v_sum[3];
// last iterations
while (i < size) {
sum += x[i] * y[i];
++i;
}
return sum;
}
6.3. Méthode utilisant les unités vectorielles SSE et dépliage par 2 sans dépendance
Ecrire la méthode dp_sse_vec_ur2_nodep qui réalise le calcul du produit scalaire en
utilisant $4 × 32$ bits d'un vecteur SSE et en dépliant la boucle de calcul par 2 et en utilisant
des registres vectoriels différents (indépendants) pour chaque partie du dépliage.
/**
* Use of SSE vector units
* unroll by a factor of 2
* perform independant calculations
*/
f32 dp_sse_vec(f32 *x, f32 *y, u32 size) {
u32 i;
// for independant calculcations we use two vectors
// for example xmm0 for v_sum and xmm2 for w_sum
f32 v_sum[4], w_w_sum[4];
v_sum[0] = v_sum[1] = v_sum[2] = v_sum[3] = 0.0;
w_sum[0] = w_sum[1] = w_sum[2] = w_sum[3] = 0.0;
// loop unrolling by a factor of 2x4
// for example
// we load x[i:i+3] in xmm1 and x[i+4:i+7] in xmm3
for (i = 0; i < (size & ~7); i += 8) {
v_sum[0] += x[i + 0] * y[i + 0];
v_sum[1] += x[i + 1] * y[i + 1];
v_sum[2] += x[i + 2] * y[i + 2];
v_sum[3] += x[i + 3] * y[i + 3];
w_sum[0] += x[i + 4] * y[i + 4];
w_sum[1] += x[i + 5] * y[i + 5];
w_sum[2] += x[i + 6] * y[i + 6];
w_sum[3] += x[i + 7] * y[i + 7];
}
// partial sums of both vectors
float v_sum_total = v_sum[0] + v_sum[1] + v_sum[2] + v_sum[3];
float w_sum_total = w_sum[0] + w_sum[1] + w_sum[2] + w_sum[3];
float sum = v_sum_total + w_sum_total;
// last iterations
while (i < size) {
sum += x[i] * y[i];
++i;
}
return sum;
}
6.4. Méthode utilisant les unités vectorielles AVX et dépliage par 2 sans dépendance
Ecrire la méthode dp_avx_vec_ur2_nodep qui réalise le calcul du produit scalaire en
utilisant $8 × 32$ bits d'un vecteur AVX et en dépliant la boucle de calcul par 2 et en utilisant
des registres vectoriels différents (indépendants) pour chaque partie du dépliage.
/**
* Use of AVX vector units
* unroll by a factor of 2
* perform independant calculations
*/
f32 dp_avx_vec(f32 *x, f32 *y, u32 size) {
// use ymm0 to store v_sum and ymm2 for w_sum
f32 v_sum[8], w_sum[8];
u32 i;
// use xorps ymm0, ymm0
v_sum[0] = v_sum[1] = v_sum[2] = v_sum[3] = 0.0;
v_sum[4] = v_sum[5] = v_sum[6] = v_sum[7] = 0.0;
// use xorps ymm2, ymm2
w_sum[0] = w_sum[1] = w_sum[2] = w_sum[3] = 0.0;
w_sum[4] = w_sum[5] = w_sum[6] = w_sum[7] = 0.0;
// loop unrolling by a factor of 16
// use ymm1 to store x[i:i+7] and ymm3 to store y[i+8:i+15]
for (i = 0; i < (size & ~15); i += 16) {
v_sum[0] += x[i + 0] * y[i + 0];
v_sum[1] += x[i + 1] * y[i + 1];
v_sum[2] += x[i + 2] * y[i + 2];
v_sum[3] += x[i + 3] * y[i + 3];
v_sum[4] += x[i + 4] * y[i + 4];
v_sum[5] += x[i + 5] * y[i + 5];
v_sum[6] += x[i + 6] * y[i + 6];
v_sum[7] += x[i + 7] * y[i + 7];
w_sum[0] += x[i + 8] * y[i + 8];
w_sum[1] += x[i + 9] * y[i + 9];
w_sum[2] += x[i + 10] * y[i + 10];
w_sum[3] += x[i + 11] * y[i + 11];
w_sum[4] += x[i + 12] * y[i + 12];
w_sum[5] += x[i + 13] * y[i + 13];
w_sum[6] += x[i + 14] * y[i + 14];
w_sum[7] += x[i + 15] * y[i + 15];
}
// sum of partial sums
float v_sum_total = v_sum[0] + v_sum[1] + v_sum[2] + v_sum[3] +;
v_sum[4] + v_sum[5] + v_sum[6] + v_sum[7];
float w_sum_total = w_sum[0] + w_sum[1] + w_sum[2] + w_sum[3] +;
w_sum[4] + w_sum[5] + w_sum[6] + w_sum[7];
float sum = v_sum_total + w_sum_total;
// last iterations
while (i < size) {
sum += x[i] * y[i];
++i;
}
return sum;
}
6.5. Méthode utilisant les unités vectorielles AVX, le dépliage par 2 sans dépendance et le FMA
Ecrire la méthode dp_avx_fma_ur2_nodep qui réalise le calcul du produit scalaire en utilisant $8 × 32$ bits d'un vecteur AVX, en dépliant la boucle de calcul par 2 et en utilisant
des registres vectoriels différents (indépendants) pour chaque partie du dépliage. Le calcul utilisera
l'instruction suivante FMA (Fuse Multiply Add):
- vfmadd231ps ymm0, ymm1, ymm2 qui additionne au registre ymm0 le produit de ymm1 par ymm2
6.6. Résultats
Voici quelques résultats obtenus sur différentes architectures en utilisant g++-10 et les drapeaux
de compilation -O3 -mavx2 -ftree-vectorize -funroll-loops -march=native :
dp_ref |
4.54 |
4.51 |
6.80 |
6.26 |
7.69 |
dp_fpu |
6.08 |
5.96 |
6.82 |
6.25 |
7.86 |
dp_sse_low |
6.08 |
5.94 |
4.08 |
3.75 |
3.56 |
dp_sse |
1.56 |
1.76 |
1.03 |
0.94 |
0.90 |
ratio dp_ref / dp_sse |
2.91 |
2.56 |
6.60 |
6.65 |
8.54 |
dp_avx |
0.87 |
1.28 |
0.57 |
0.49 |
0.45 |
dp_avx_ur2_no_dep |
0.80 |
1.33 |
0.51 |
0.41 |
0.40 |
dp_avx_fma_ur2_no_dep |
0.81 |
1.21 |
0.50 |
0.42 |
0.37 |
dp_intrin_avx |
0.83 |
1.16 |
0.87 |
0.49 |
0.46 |
Résultats pour 10.000 répétitions de la boucle sur des vecteurs de 524.288 floats
Note : on obtient parfois un problème d'affichage du temps d'exécution, notamment des décimales sur certaines configurations. Cela est dû à la localisation. Il est alors nécessaire d'utiliser la commande :
LANG=en_US ./test_dot_product.sh
6.6.1. Analyse des résultats
On note que pour les processeurs Intel, utiliser la FPU ou la partie basse des registres SSE donne les mêmes résultats. Il se pourrait donc que les mêmes circuits soient utilisés dans les deux cas ou qu'ils aient été conçus de la même manière.
Au contraire, pour AMD, les circuits FPU sont beaucoup plus lents que les registres SSE.
Pour certains processeurs, l'utilisation de l'AVX apporte un gain de presque deux (on va deux fois plus vite) par rapport au SSE. Sauf pour l'Intel i3-6100.