Esta es una buena pregunta. Hay muchas razones por las que desearía realmente transponer la matriz en la memoria en lugar de simplemente intercambiar coordenadas, por ejemplo, en la multiplicación de matrices y la difusión de Gauss.
Primero permítanme enumerar una de las funciones que uso para la transposición ( EDITAR: vea el final de mi respuesta donde encontré una solución mucho más rápida )
void transpose(float *src, float *dst, const int N, const int M) {
#pragma omp parallel for
for(int n = 0; n<N*M; n++) {
int i = n/N;
int j = n%N;
dst[n] = src[M*j + i];
}
}
Ahora veamos por qué es útil la transposición. Considere la multiplicación de matrices C = A * B. Podríamos hacerlo de esta manera.
for(int i=0; i<N; i++) {
for(int j=0; j<K; j++) {
float tmp = 0;
for(int l=0; l<M; l++) {
tmp += A[M*i+l]*B[K*l+j];
}
C[K*i + j] = tmp;
}
}
De esa forma, sin embargo, habrá muchas pérdidas de caché. Una solución mucho más rápida es tomar primero la transposición de B
transpose(B);
for(int i=0; i<N; i++) {
for(int j=0; j<K; j++) {
float tmp = 0;
for(int l=0; l<M; l++) {
tmp += A[M*i+l]*B[K*j+l];
}
C[K*i + j] = tmp;
}
}
transpose(B);
La multiplicación de matrices es O (n ^ 3) y la transposición es O (n ^ 2), por lo que tomar la transposición debería tener un efecto insignificante en el tiempo de cálculo (para grandes n
). En el ciclo de multiplicación de matrices, el mosaico es incluso más efectivo que tomar la transposición, pero eso es mucho más complicado.
Ojalá supiera una forma más rápida de hacer la transposición ( Editar: encontré una solución más rápida, vea el final de mi respuesta ). Cuando salga Haswell / AVX2 en unas pocas semanas, tendrá una función de recopilación. No sé si eso será útil en este caso, pero podría imaginarme reuniendo una columna y escribiendo una fila. Tal vez haga innecesaria la transposición.
Para el difuminado gaussiano, lo que se hace es untar horizontalmente y luego untar verticalmente. Pero manchar verticalmente tiene el problema de la caché, por lo que
Smear image horizontally
transpose output
Smear output horizontally
transpose output
Aquí hay un documento de Intel que explica que
http://software.intel.com/en-us/articles/iir-gaussian-blur-filter-implementation-using-intel-advanced-vector-extensions
Por último, lo que realmente hago en la multiplicación de matrices (y en la difuminación gaussiana) no es tomar exactamente la transposición, sino tomar la transposición en anchos de un cierto tamaño de vector (por ejemplo, 4 u 8 para SSE / AVX). Aquí está la función que uso
void reorder_matrix(const float* A, float* B, const int N, const int M, const int vec_size) {
#pragma omp parallel for
for(int n=0; n<M*N; n++) {
int k = vec_size*(n/N/vec_size);
int i = (n/vec_size)%N;
int j = n%vec_size;
B[n] = A[M*i + k + j];
}
}
EDITAR:
Probé varias funciones para encontrar la transposición más rápida para matrices grandes. Al final, el resultado más rápido es usar el bloqueo de bucle con block_size=16
( Editar: encontré una solución más rápida usando SSE y bloqueo de bucle, ver más abajo ). Este código funciona para cualquier matriz NxM (es decir, la matriz no tiene que ser cuadrada).
inline void transpose_scalar_block(float *A, float *B, const int lda, const int ldb, const int block_size) {
#pragma omp parallel for
for(int i=0; i<block_size; i++) {
for(int j=0; j<block_size; j++) {
B[j*ldb + i] = A[i*lda +j];
}
}
}
inline void transpose_block(float *A, float *B, const int n, const int m, const int lda, const int ldb, const int block_size) {
#pragma omp parallel for
for(int i=0; i<n; i+=block_size) {
for(int j=0; j<m; j+=block_size) {
transpose_scalar_block(&A[i*lda +j], &B[j*ldb + i], lda, ldb, block_size);
}
}
}
Los valores lda
y ldb
son el ancho de la matriz. Estos deben ser múltiplos del tamaño del bloque. Para encontrar los valores y asignar la memoria para, por ejemplo, una matriz de 3000x1001, hago algo como esto
#define ROUND_UP(x, s) (((x)+((s)-1)) & -(s))
const int n = 3000;
const int m = 1001;
int lda = ROUND_UP(m, 16);
int ldb = ROUND_UP(n, 16);
float *A = (float*)_mm_malloc(sizeof(float)*lda*ldb, 64);
float *B = (float*)_mm_malloc(sizeof(float)*lda*ldb, 64);
Para 3000x1001 esto devuelve ldb = 3008
y lda = 1008
Editar:
Encontré una solución aún más rápida usando intrínsecos SSE:
inline void transpose4x4_SSE(float *A, float *B, const int lda, const int ldb) {
__m128 row1 = _mm_load_ps(&A[0*lda]);
__m128 row2 = _mm_load_ps(&A[1*lda]);
__m128 row3 = _mm_load_ps(&A[2*lda]);
__m128 row4 = _mm_load_ps(&A[3*lda]);
_MM_TRANSPOSE4_PS(row1, row2, row3, row4);
_mm_store_ps(&B[0*ldb], row1);
_mm_store_ps(&B[1*ldb], row2);
_mm_store_ps(&B[2*ldb], row3);
_mm_store_ps(&B[3*ldb], row4);
}
inline void transpose_block_SSE4x4(float *A, float *B, const int n, const int m, const int lda, const int ldb ,const int block_size) {
#pragma omp parallel for
for(int i=0; i<n; i+=block_size) {
for(int j=0; j<m; j+=block_size) {
int max_i2 = i+block_size < n ? i + block_size : n;
int max_j2 = j+block_size < m ? j + block_size : m;
for(int i2=i; i2<max_i2; i2+=4) {
for(int j2=j; j2<max_j2; j2+=4) {
transpose4x4_SSE(&A[i2*lda +j2], &B[j2*ldb + i2], lda, ldb);
}
}
}
}
}