5.3.5 Vectorisation

Nous allons enfin utiliser toute la puissance de notre ordinateur (enfin presque toute). Mais la vectorisation ne se fait pas toute seule. Nous allons utiliser l'option de gcc qui permet de vectoriser le programme, mais il va falloir que nous modifions un peut notre fonction.

Le compilateur est très prudent en ce qui concerne la vectorisation, en gros, si vous ne lui définissez pas tout bien comme il faut, vous pourrez mettre toutes les options de vectorisation que vous voudrez il n'en aura rien à faire.

Nous devons garantir deux choses :

  • Il faut que les pointeurs ne se recouvrent pas (mais il faut l'expliciter)
  • Il faut que les tableaux soient aligné, pour que les vecteurs puissent être remplis

Pour le premier point, nous allons utiliser le mot clé __restrict__ qui permet de dire au compilateur que les pointeurs pointent vers des tableaux indépendants qui ne se recouvrent pas (Bien sûr, si vous lui mentez, ça va mal se passer).

Nous allons créer une nouvelle fonction donc le prototype sera le suivant :

1
void sgemmOptimVectorize(float* __restrict__ pr, const float* __restrict__ pa, const float* __restrict__ pb, size_t size);

Notez que les variables n'ont plus les mêmes noms, mais la encore, c'est de la cosmétique.

Pour garantir le deuxième point, nous allons utiliser la fonction __builtin_assume_aligned, qui créer un pointeur aligné à partir d'un pointeur et d'un alignement donné.

Voici l'implémentation de cette nouvelle fonction :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void sgemmOptimVectorize(float* __restrict__ pr, const float* __restrict__ pa, const float* __restrict__ pb, size_t size){
	float* r = (float*)__builtin_assume_aligned(pr, 16);
	float* a = (float*)__builtin_assume_aligned(pa, 16);
	float* b = (float*)__builtin_assume_aligned(pb, 16);
	
	size_t i,j,k;
	for(i = 0; i < size*size; ++i){
		r[i] = 0.0f;
	}
	size_t jSize,kSize;
	for(j = 0; j < size; ++j){
		jSize = j*size;
		for(k = 0; k < size; ++k){
			kSize = k*size;
			for(i = 0; i < size; ++i){
				r[jSize + i] += a[jSize + k]*b[kSize + i];
			}
		}
	}
}

Si vous appelez cette fonction dans le main, sans modifier les options de compilation, vous obtiendrez quand même un gain :

./optimisationsgemm 
2.326441 cy/fma

Nous étions précédemment à 2.926861 cy/fma, nous avons encore gagner un peu (21 pour-cents).

C'est tout à fait normal car les données son mieux alignées et on garanti qu'elle sont indépendantes.

Changeons les options de compilation :

1
add_definitions(-Wall -O2 -ffast-math -funroll-loops)

par :

1
add_definitions(-Wall -ffast-math -funroll-loops -ftree-vectorize -ftree-vectorizer-verbose=4 -O2 -march=native)

Ce qui nous donne, après re-compilation :

./optimisationsgemm 
1.086403 cy/fma

Nous étions à 2.326441 cy/fma, nous avons donc gagner 54 pour-cents ce qui est énorme. Nous pouvons maintenant avoir un calcul à chaque cycle.

Et si je le relance, c'est encore mieux :

./optimisationsgemm 
1.046540 cy/fma

On est toujours au alentour d'un calcul par cycle ce qui est bien.