Luchian dá uma explicação de por que esse comportamento ocorre, mas achei que seria uma boa idéia mostrar uma solução possível para esse problema e, ao mesmo tempo, mostrar um pouco sobre algoritmos alheios ao cache.
Seu algoritmo basicamente faz:
for (int i = 0; i < N; i++)
for (int j = 0; j < N; j++)
A[j][i] = A[i][j];
o que é simplesmente horrível para uma CPU moderna. Uma solução é conhecer os detalhes sobre o seu sistema de cache e ajustar o algoritmo para evitar esses problemas. Funciona muito bem desde que você conheça esses detalhes. Não é especialmente portátil.
Podemos fazer melhor que isso? Sim, podemos: Uma abordagem geral para esse problema são algoritmos alheios ao cache que, como o nome indica, evitam depender de tamanhos específicos de cache [1]
A solução seria assim:
void recursiveTranspose(int i0, int i1, int j0, int j1) {
int di = i1 - i0, dj = j1 - j0;
const int LEAFSIZE = 32; // well ok caching still affects this one here
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];
}
}
Um pouco mais complexo, mas um pequeno teste mostra algo bastante interessante no meu antigo e8400 com o lançamento do VS2010 x64, código de teste para MATSIZE 8192
int main() {
LARGE_INTEGER start, end, freq;
QueryPerformanceFrequency(&freq);
QueryPerformanceCounter(&start);
recursiveTranspose(0, MATSIZE, 0, MATSIZE);
QueryPerformanceCounter(&end);
printf("recursive: %.2fms\n", (end.QuadPart - start.QuadPart) / (double(freq.QuadPart) / 1000));
QueryPerformanceCounter(&start);
transpose();
QueryPerformanceCounter(&end);
printf("iterative: %.2fms\n", (end.QuadPart - start.QuadPart) / (double(freq.QuadPart) / 1000));
return 0;
}
results:
recursive: 480.58ms
iterative: 3678.46ms
Edit: Sobre a influência do tamanho: é muito menos pronunciado, embora ainda perceptível até certo ponto, é porque estamos usando a solução iterativa como um nó folha em vez de recursar até 1 (a otimização usual para algoritmos recursivos). Se definirmos LEAFSIZE = 1, o cache não terá influência para mim [ 8193: 1214.06; 8192: 1171.62ms, 8191: 1351.07ms
- isso está dentro da margem de erro, as flutuações estão na área de 100ms; esse "benchmark" não é algo com o qual eu me sentiria muito confortável se quiséssemos valores completamente precisos])
[1] Fontes para essas coisas: Bem, se você não conseguir uma palestra de alguém que trabalhou com Leiserson e co-nisto ... presumo que os trabalhos deles sejam um bom ponto de partida. Esses algoritmos ainda são raramente descritos - o CLR tem uma única nota de rodapé sobre eles. Ainda é uma ótima maneira de surpreender as pessoas.
Editar (nota: não fui eu quem postou esta resposta; eu só queria adicionar esta):
Aqui está uma versão completa em C ++ do código acima:
template<class InIt, class OutIt>
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];
}
}
}
}