Salut,
J'ai une quantité bi-dimensionnelle : f(i,j) que je veux accéder en lecture et en écriture, rapidement. Le domaine sur lequel f est défini contient beaucoup d'éléments (possiblement plusieurs millions de 'double').
A chaque élément f(i,j), je vais avoir besoin d'accéder rapidement (en lecture surtout) aux élements "autour", c'est à dire :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5 f(i+1,j) f(i-1,j) f(i,j+1) f(i,j-1)
Je me des questions quant à l'implémentation la plus efficace de ces données du point de vue rapidité.
J'ai trois solutions (peut-on en imaginer d'autres ?)
La première consiste a faire un "vrai" tableau 2D dynamique :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9 typedef unsigned int uint; typedef struct array2D { double **a; uint nx; uint ny; } Array2D;
que je construit comme ceci :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35 Array2D *array2D_new(uint nx, uint ny) { Array2D *this; uint i; uint k; this = calloc(1, sizeof *this); if (this == NULL) return NULL; this->a = calloc(nx, sizeof *this->a); if (this->a == NULL) { free(this); return NULL; } for (i=0; i < nx; i ++) { this->a[i] = calloc(ny, sizeof *this->a[i]); if (this->a[i] == NULL) { for (k=0; k <= i; k ++) free (this->a[k]); free(this); return NULL; } } this->nx = nx; this->ny = ny; return this; }
détruit ainsi :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 void array2D_delete(Array2D *this) { uint i; if (this != NULL) { for (i=0; i < this->nx; i++) { if (this->a[i]) free(this->a[i]); } } free(this); }
et pour lequel j'accède aux éléments de cette manière :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 void array2D_stencil(const Array2D *grid) { uint i,j; double tmp; uint nx, ny; nx = grid->nx; ny = grid->ny; for (i=1; i < nx-1; i ++) { for (j = 1; j < ny-1; j++) { // do some stuff with the four // neighbouring points tmp = 0.25*(grid->a[i-1][j ] + grid->a[i ][j+1] + grid->a[i+1][j ] + grid->a[i ][j-1]); } } }
La seconde méthode, consiste à faire un tableau dynamique 1D, et que l'utilisateur calcule lui même l'indice 1D à partir de deux boucles imbriquées sur i et j.
Ca se déclare comme ceci :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7 typedef struct _array2D1D { double *a; uint nx; uint ny; } Array2D1D;
se construit ainsi :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 Array2D1D *array2D1D_new(uint nx, uint ny) { Array2D1D *this; this = calloc(1, sizeof *this); if (!this) return NULL; this->a = calloc(nx*ny, sizeof *this->a); if (! this->a) { free(this); return NULL; } this->nx = nx; this->ny = ny; return this; }
se détruit ainsi
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11 void array2D1D_delete(Array2D1D *this) { if (this) { if (this->a) free (this->a); free(this); } }
et on accède aux éléments ainsi :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46 void array2D1D_stencil_a(const Array2D1D *grid) { uint i,j; uint ij, ij1, ij2, ij3, ij4; double tmp; double *a; uint nx, ny; nx = grid->nx; ny = grid->ny; a = grid->a; // +ij2 // | // | // ij1 | ij3 // +--------+ij------+ // | // | // | // + // ij4 for (i=1; i < nx-1; i ++) { for (j = 1; j < ny-1; j++) { ij = (i ) + (j )*(nx); ij1 = (i-1) + (j )*(nx); ij2 = (i ) + (j+1)*(nx); ij3 = (i+1) + (j )*(nx); ij4 = (i ) + (j-1)*(nx); // do some stuff with the four // neighbouring points tmp = 0.25*(a[ij1] + a[ij2] + a[ij3] + a[ij4]); } } }
J'ai chronométré les fonctions *_stencil() pour les deux objets, sur des tableaux de 8192*8192 éléments, et ça 10fois de suite, afin de m'assurer que le chronométrage ne dépend pas d'autres processus tournant sur ma machine.
j'ai été très surpris de constater que la méthode 1 est BIEN plus rapide. En effet, voici les temps, en seconde des deux méthodes :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12 méthode 1 méthode 2 1.10430538 8.25240617 0.967257762 7.72914395 0.963137127 7.71300082 0.963082132 7.70107212 0.963722737 7.69799643 0.969022776 7.80377674 0.976541885 7.83630202 0.963828863 7.70916793 0.964121218 7.71000535 0.963386301 7.70990839
En fait, j'aurai dit que la méthode 1 serait la plus lente. Car la "seconde dimension" du tableau est allouée à un endroit différent dans la mémoire que la première dimension... et chaque ligne de la seconde dimension est allouée à un endroit différent. En allant chercher des valeur aux points (i+1,j), (i-1,j), (i,j+1), (i,j-1) j'aurais donc pensé qu'on allait charger des zones différentes en cache, et perdre en rapidité.
La méthode 2 en revanche, présente en effet un surcout car elle demande, pour charger un élément à la ligne supérieure (j+1), de charger toutes les données i de la ligne j. Je pensais que ça ne serait pas efficace, mais tout de même plus que la méthode 1, car même si on doit sauter beaucoup de point, les données restent contigues en mémoire.
Devant le résultat, j'ai pensé que le surcout venait peut-être du calcul d'indice que je fais à chaque fois :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5 ij1 = (i-1) + (j )*(nx); ij2 = (i ) + (j+1)*(nx); ij3 = (i+1) + (j )*(nx); ij4 = (i ) + (j-1)*(nx);
J'ai donc légèrement modifié mon objet, de sorte à ce que les indices des points autour de (i,j) de soient plus calculés à chaque fois, mais calculés une bonne fois pour toute lors de la création de l'objet, pour chaque point (i,j) :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8 typedef struct _array2D1D { double *a; uint nx; uint ny; uint *stencil[4]; } Array2D1D;
le "stencil" est donc un ensemble de 4 indices par point de tableau 'a', il est créé dans le constructeur précédent, un peu modifié pour la cause :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62 Array2D1D *array2D1D_new(uint nx, uint ny) { Array2D1D *this; uint i,j,ij; uint ij1, ij2, ij3, ij4; this = calloc(1, sizeof *this); if (!this) return NULL; this->a = calloc(nx*ny, sizeof *this->a); if (! this->a) { free(this); return NULL; } this->nx = nx; this->ny = ny; // allocation pas verifiee.. a l'arrache this->stencil[0] = calloc(nx*ny, sizeof *this->stencil[0]); this->stencil[1] = calloc(nx*ny, sizeof *this->stencil[1]); this->stencil[2] = calloc(nx*ny, sizeof *this->stencil[2]); this->stencil[3] = calloc(nx*ny, sizeof *this->stencil[3]); // +ij2 // | // | // ij1 | ij3 // +-----+ij------+ // | // | // | // + // ij4 for (i=0; i < nx; i++) { for (j=0; j < ny; j++) { ij = (i ) + (j )*(nx); ij1 = (i-1) + (j )*(nx); ij2 = (i ) + (j+1)*(nx); ij3 = (i+1) + (j )*(nx); ij4 = (i ) + (j-1)*(nx); this->stencil[0][ij] = ij1; this->stencil[1][ij] = ij2; this->stencil[2][ij] = ij3; this->stencil[3][ij] = ij4; } } return this; }
Cette méthode, je pensais, permet de s'affranchir du cout des calculs d'indices, au prix d'un cout mémoire additionnel que je suis prêt à payer.
l'accès au tableau se fait à présent ainsi :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47 void array2D1D_stencil_b(const Array2D1D *grid) { uint i,j; double tmp; double *a; uint *ij1; uint *ij2; uint *ij3; uint *ij4; uint nx, ny; nx = grid->nx; ny = grid->ny; a = grid->a; ij1 = grid->stencil[0]; ij2 = grid->stencil[1]; ij3 = grid->stencil[2]; ij4 = grid->stencil[3]; // +ij2 // | // | // ij1 | ij3 // +--------+ij------+ // | // | // | // + // ij4 for (i=1; i < nx-1; i ++) { for (j = 1; j < ny-1; j++) { // do some stuff with the four // neighbouring points tmp = 0.25*(a[ij1[i + j*nx]] + a[ij2[i+j*nx]] + a[ij3[i+j*nx]] + a[ij4[i+j*nx]]); } } }
Je relance mon code de mesure de temps, et à mes deux colonnes précédentes s'ajoute une troisième, mesurant le temps en secondes passé dans la routine array2D1D_stencil_b()
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12 methode 1 méthode 2 méthode 3 1.10430538 8.25240617 26.6270934 0.967257762 7.72914395 26.6097884 0.963137127 7.71300082 26.6851487 0.963082132 7.70107212 26.6141512 0.963722737 7.69799643 26.7747686 0.969022776 7.80377674 26.8523572 0.976541885 7.83630202 26.7336635 0.963828863 7.70916793 26.6687285 0.964121218 7.71000535 26.6810654 0.963386301 7.70990839 26.7819276
Alors ça ! c'est encore beaucoup plus lent que la seconde méthode !!
en gros, je pensais que les temps seraient :
méthode3 < méthode2 < méthode 1
et finalement c'est tout l'inverse !
Quelqu'un peut-il m'expliquer ça ? Il semble finalement qu'il faille privilégier la méthode 1, et de loin....
J'ai mis le code dans un zip en pièce jointe. Ca devrait compiler sans probleme avec un simple 'make' sous linux (ça ne tourne pas sous mac, et je ne sais pas pour windows, à cause de la bibliothèque 'lrt').
Merci![]()
Partager