Conséquence de l’architecture mémoire Adrien Poteaux et Alexandre Sedoglavic Introduction Conséquence sur le code Conséquence de l’architecture mémoire Adrien Poteaux et Alexandre Sedoglavic Licence 3 info Université de Lille 1 Janvier 2015 Conséquence de l’architecture mémoire Adrien Poteaux et Alexandre Sedoglavic Mesures de performances I Introduction temporelles Instructions per second (ips) ; cette mesure est dépendante du programme et du langage. On estime que un quadricœur intel core 2 cadencé à 2.5GHz est de l’ordre de 40 gips. Conséquence sur le code I FLoating-point OPeration (flop) ; cette mesure permet des comparaison mais ne prend en compte que les instructions de calculs. Les microprocesseurs actuels réalisent 4 flops par cycle d’horloge. I FLoating-point Operation Per Second (flops). Un cœur cadencé à 2.5GHz présente donc une performance théorique de 10 milliards de flops (i.e. 10). gflops. Un quadicœur dispose donc théoriquement de 40 gflops. Conséquence de l’architecture mémoire Adrien Poteaux et Alexandre Sedoglavic Mesures de performances I Introduction Conséquence sur le code I spatiales la latence est le temps nécessaire à des données pour passer de la source à la destination (on considère ici la latence des mémoires ram). la bande passante est le débit de transmission des données entre la source et la destination. La bande passante est donc le produit : I I I I de la latence des mémoire ; de la quantité de données transmise par cycle (2 mots de 32 lettres dans le cas des ddr ram ; de la taille du bus (64 lettres actuellement) ; nombre de bus — canaux — irriguant le processeur. Ainsi, une architecture abordable actuellement est constitué de 2 canaux de 64 lettres liant le processeur à des mémoires ddr2 ram cadencés à 400mhz ; cela correspond à une bande passante de 12.8 gb par seconde. Conséquence de l’architecture mémoire Adrien Poteaux et Alexandre Sedoglavic Introduction Conséquence sur le code Constats La plupart des applications n’exploitent que 10% des performances optimales du processeurs. I Les limitations principale sont dues à la circulation des données en mémoire. Écart de performance entre processeurs et mémoire : I Les performances de la bande passante progresse plus vite — Conséquence de l’architecture mémoire Adrien Poteaux et Alexandre Sedoglavic Hiérarchie de mémoire Partant du constat de localité : I spatiale i.e. les prochaı̂nes données à utiliser sont souvent proches des données actuelles ; I temporelle i.e. une donnée actuellement utilisée à de grande chance d’être réutilisée, Introduction Conséquence sur le code l’architecture implante un mécanisme de tampon type latence taille cache L1 10−9 s kilo-octets cache L2 10−8 s méga-octets ram 10−7 s giga-octets disque 10−2 s tera-octets afin d’améliorer les performances moyennes. La taille est inversement proportionnelle à la latence (pour être plus rapide, il faut partager le même support physique ce qui limite la taille). Conséquence de l’architecture mémoire Adrien Poteaux et Alexandre Sedoglavic Introduction L’architecture du système de mémoire cherche à I Conséquence sur le code économiser les accès mémoire en stockant les données dans de petite mémoire rapide (cache) et en réutilisants ces dernières : cela implique une localité temporelle de l’utilisation des données dans un code. I utiliser la bande passante pour stocker des blocks (page du cache) de données ; cela implique une localité spatiale de l’utilisation des données dans un code. Conséquence de l’architecture mémoire Adrien Poteaux et Alexandre Sedoglavic Introduction Conséquence sur le code l’outil Pahole On peut désassembler le code pour avoir de l’information : struct matrice{ int tab[1024][1024] ; } mat ; int main(void){ mat.tab[0][0] = 1 ; return 0 ; } // gcc -g -o mat mat.c ; pahole mat struct matrice { int tab[1024][1024]; /* 0 4194304 */ /*- cacheline 65536 boundary (4194304 bytes) -*/ /* size: 4194304, cachelines: 65536, members: 1 */ }; Conséquence de l’architecture mémoire Fusion de boucles Adrien Poteaux et Alexandre Sedoglavic Introduction Conséquence sur le code Algorithm 1 Favoriser la localité 1: 2: 3: 4: 5: 6: 7: double a[n],b[n],c[n] ; for i=1 to n do b[i] = a[i] + 1.0 ; end for for i=1 to n do c[i] = b[i] *4.0 ; end for double a[n],b[n],c[n] ; for i=1 to n do 3: b[i] = a[i] + 1.0 ; 4: c[i] = b[i] *4.0 ; 5: end for 1: 2: Conséquence de l’architecture mémoire Intervertion de boucle Adrien Poteaux et Alexandre Sedoglavic Introduction Conséquence sur le code Algorithm 2 Parcours de matrices 1: 2: 3: 4: 5: 6: 7: double sum ; double a[n,n] ; for j=1 to n do for i=1 to n do sum+= a[i,j] ; end for end for 1: 2: 3: 4: 5: 6: 7: double sum ; double a[n,n] ; for i=1 to n do for j=1 to n do sum+= a[i,j] ; end for end for Conséquence de l’architecture mémoire Insertion d’espace Adrien Poteaux et Alexandre Sedoglavic Introduction Conséquence sur le code Algorithm 3 Éviter de mélanger écriture et lecture 1: 2: 3: 4: 5: double sum ; double a[n],b[n] ; for i=1 to n do sum+= a[i]*b[j] ; end for 1: 2: 3: 4: 5: 6: 7: double sum ; double a[n] ; double pad[x] ; double b[n] ; for i=1 to n do sum+= a[i]*b[j] ; end for Conséquence de l’architecture mémoire Adrien Poteaux et Alexandre Sedoglavic Introduction Conséquence sur le code Construction de blocs Une matrice carrée A de taille n × n est stockée unidimensionnellement en mémoire : I à la fortran : A(i, j) = A + i + j × n (par colonne) ; I à la c : A(i, j) = A + i × n + j (par ligne) ; Algorithm 4 Blocs et transposition 1: double a[n,n],b[n,n] ; 2: for i=1 to n do 3: for j=1 to n do 4: a[i,j] = b[j,i] ; 5: end for 6: end for 1: double a[n,n],b[n,n] ; 2: for ii=1 to n by B do 3: for jj=1 to n by B do 4: for i=ii to min(ii+B-1,n) do 5: 6: 7: 8: 9: 10: for j=jj to min(jj+B1,n) do a[i,j] = b[j,i] ; end for end for end for end for Conséquence de l’architecture mémoire Adrien Poteaux et Alexandre Sedoglavic Introduction Conséquence sur le code Représentation de tableaux Dans la déclaration int i,j ;, les variables i et j peuvent être dans une ligne de cache différente. Pour s’assurer qu’elle soit dans la même ligne, on peut utiliser : __declspec(align(16)) struct {int i,j ;} sub ; // __declspec(align(16)) permet d’agir // sur l’alignement Algorithm 5 Différentes variantes 1: double a[n],b[n] ; 1: double ab[n][2] ; 1: struct { 2: double a ; 3: double b ; 4: } ab[n] ; Conséquence de l’architecture mémoire Adrien Poteaux et Alexandre Sedoglavic Introduction Conséquence sur le code C et la manipulation de cache Le fichier d’entête emmintrin.h permet d’accéder à des instructions assembleur de type Single Instruction on Multiple Data (simd) et Streaming SIMD Extensions (sse) ; par exemple : void __mm_prefetch(char const *p, int); /* 0 - read only, 1 - rw */ // charge une ligne de cache depuis l’adresse p void __mm__clflush(void const *p) ; // la ligne de cache contenant p est vid\’ee void __mm__sfence(void) ; // garantie que chaque op\’eration m\’emoire // pr\’ec\’edent l’appel est visible globalement void __mm__stream_ps(float *p, __m128 a) ; // charge la donn\’ee de a dans l’adresse p // sans passer par le cache Conséquence de l’architecture mémoire Adrien Poteaux et Alexandre Sedoglavic Introduction Conséquence sur le code Modifier une ligne de cache Le code suivant : include <emmintrin.h> void setbytes(char *p, int c){ __m128i i = _mm_set_epi8(c, c, c, c, c, c, c, c, c, c, c, c, c, c, c, c); _mm_stream_si128((__m128i *)&p[0], i); _mm_stream_si128((__m128i *)&p[16], i); _mm_stream_si128((__m128i *)&p[32], i); _mm_stream_si128((__m128i *)&p[48], i); } permet de mettre à c tout les octets de la ligne de cache contenant l’adresse p (supposée alignée). La fonction memset utilise ce type d’astuce. Conséquence de l’architecture mémoire Exemple d’améliorations Adrien Poteaux et Alexandre Sedoglavic Nous allons utiliser l’algorithme classique suivant : Introduction Conséquence sur le code Algorithm 6 Multiplication naı̈ve de matrice 1: int i,j,k ; 2: int a[n],b[n],res[n] ; 3: for i=1 to n do 4: for j=1 to n do 5: for k=1 to n do 6: res[i][j] += a[i][k]*b[k][j] ; 7: end for 8: end for 9: end for Conséquence de l’architecture mémoire Adrien Poteaux et Alexandre Sedoglavic Introduction Conséquence sur le code On suppose disposer de la macro CLS à la compilation (-DCLS=$(getconf LEVEL1_DCACHE_LINESIZE)). #define SM (CLS / sizeof (int)) for (i = 0; i < N; i += SM) for (j = 0; j < N; j += SM) for (k = 0; k < N; k += SM) rres = &res[i][j] ; rmul1 = &mul1[i][k] ; for (i2 = 0 ; i2 < SM; ++i2, rres += N, rmul1 += N) for (k2 = 0, rmul2 = &mul2[k][j]; k2 < SM; ++k2, rmul2 += N) for (j2 = 0; j2 < SM; ++j2) rres[j2] += rmul1[k2] * rmul2[j2];