Luchian donne une explication de pourquoi ce comportement se produit, mais j'ai pensé que ce serait une bonne idée de montrer une solution possible à ce problème et en même temps de montrer un peu sur les algorithmes sans cache.
Votre algorithme fait essentiellement:
for (int i = 0; i < N; i++)
for (int j = 0; j < N; j++)
A[j][i] = A[i][j];
ce qui est tout simplement horrible pour un CPU moderne. Une solution consiste à connaître les détails de votre système de cache et à ajuster l'algorithme pour éviter ces problèmes. Fonctionne très bien tant que vous connaissez ces détails.. pas particulièrement portable.
Pouvons-nous faire mieux que cela ? Oui nous le pouvons: Une approche générale à ce problème est basée sur les algorithmes sans cache qui, comme le nom l'indique, évitent de dépendre des tailles de cache spécifiques [1]
La solution ressemblerait à ceci:
void recursiveTranspose(int i0, int i1, int j0, int j1) {
int di = i1 - i0, dj = j1 - j0;
const int LEAFSIZE = 32; // bon ok le caching affecte toujours celui-ci
if (di >= dj && di > LEAFSIZE) {
int im = (i0 + i1) / 2;
recursiveTranspose(i0, im, j0, j1);
recursiveTranspose(im, i1, j0, j1);
} else if (dj > LEAFSIZE) {
int jm = (j0 + j1) / 2;
recursiveTranspose(i0, i1, j0, jm);
recursiveTranspose(i0, i1, jm, j1);
} else {
for (int i = i0; i < i1; i++ )
for (int j = j0; j < j1; j++ )
mat[j][i] = mat[i][j];
}
}
Légèrement plus complexe, mais un court test montre quelque chose de très intéressant sur mon ancien e8400 avec VS2010 x64 version finale, code de test pour MATSIZE 8192
int main() {
LARGE_INTEGER start, end, freq;
QueryPerformanceFrequency(&freq);
QueryPerformanceCounter(&start);
recursiveTranspose(0, MATSIZE, 0, MATSIZE);
QueryPerformanceCounter(&end);
printf("récursif: %.2fms\n", (end.QuadPart - start.QuadPart) / (double(freq.QuadPart) / 1000));
QueryPerformanceCounter(&start);
transpose();
QueryPerformanceCounter(&end);
printf("itératif: %.2fms\n", (end.QuadPart - start.QuadPart) / (double(freq.QuadPart) / 1000));
return 0;
}
résultats:
récursif: 480.58ms
itératif: 3678.46ms
Édit: À propos de l'influence de la taille: Elle est beaucoup moins prononcée bien que toujours perceptible dans une certaine mesure, c'est parce que nous utilisons la solution itérative comme un nœud feuille au lieu de descendre en récursif jusqu'à 1 (l'optimisation habituelle pour les algorithmes récursifs). Si nous définissons LEAFSIZE = 1, le cache n'a aucune influence pour moi [8193: 1214.06; 8192: 1171.62ms, 8191: 1351.07ms
- c'est à l'intérieur de la marge d'erreur, les fluctuations sont dans la zone des 100ms; ce "test" n'est pas quelque chose avec lequel je serais trop à l'aise si nous voulions des valeurs complètement précises])
[1] Sources pour ces choses: Eh bien, si vous ne pouvez pas obtenir un cours magistral de quelqu'un qui a travaillé avec Leiserson et compagnie sur ce sujet.. je suppose que leurs documents sont un bon point de départ. Ces algorithmes sont encore assez rarement décrits - CLR a une note unique à leur sujet. Néanmoins, c'est un excellent moyen de surprendre les gens.
Édition (note: Je ne suis pas celui qui a posté cette réponse; je voulais juste ajouter ceci):
Voici une version C++ complète du code ci-dessus:
template
void transpose(InIt const input, OutIt const output,
size_t const rows, size_t const columns,
size_t const r1 = 0, size_t const c1 = 0,
size_t r2 = ~(size_t) 0, size_t c2 = ~(size_t) 0,
size_t const leaf = 0x20)
{
if (!~c2) { c2 = columns - c1; }
if (!~r2) { r2 = rows - r1; }
size_t const di = r2 - r1, dj = c2 - c1;
if (di >= dj && di > leaf)
{
transpose(input, output, rows, columns, r1, c1, (r1 + r2) / 2, c2);
transpose(input, output, rows, columns, (r1 + r2) / 2, c1, r2, c2);
}
else if (dj > leaf)
{
transpose(input, output, rows, columns, r1, c1, r2, (c1 + c2) / 2);
transpose(input, output, rows, columns, r1, (c1 + c2) / 2, r2, c2);
}
else
{
for (ptrdiff_t i1 = (ptrdiff_t) r1, i2 = (ptrdiff_t) (i1 * columns);
i1 < (ptrdiff_t) r2; ++i1, i2 += (ptrdiff_t) columns)
{
for (ptrdiff_t j1 = (ptrdiff_t) c1, j2 = (ptrdiff_t) (j1 * rows);
j1 < (ptrdiff_t) c2; ++j1, j2 += (ptrdiff_t) rows)
{
output[j2 + i1] = input[i2 + j1];
}
}
}
}
11 votes
Votre code me semble peu efficace en termes de cache.
7 votes
C'est à peu près la même question que celle-ci : stackoverflow.com/questions/7905760/…
0 votes
Avez-vous des explications à apporter, @CodesInChaos? (Ou quelqu'un d'autre.)
0 votes
@Bane Que dirais-tu de lire la réponse acceptée ?
0 votes
@CodesInChaos Oui, j'ai compris ce que tu voulais dire. Je lis généralement d'abord les commentaires.
0 votes
J'ai essayé le code avec gcc 4.6.1 sur Ubuntu 11.10, avec l'optimisation par défaut (niveau O0), la matrice 513*513 est plus lente que celle de 512*512, (2,62 ms pour la taille 513, et 2,45 ms pour la taille 512), avec l'optimisation O1 ou supérieure, le temps pour 512 est de 2,08 ms, et pour 513 est de 0,56 ms. cpu : Intel(R) Core(TM) i3 530 2,93GHz. Quelqu'un peut-il expliquer cela?
4 votes
@nzomkxia Il est un peu inutile de mesurer quoi que ce soit sans optimisations. Avec les optimisations désactivées, le code généré sera parsemé de déchets superflus qui masqueront d'autres goulots d'étranglement. (comme la mémoire)