To jest dobre pytanie. Istnieje wiele powodów, dla których chciałbyś faktycznie transponować macierz w pamięci, a nie tylko zamienić współrzędne, np. W mnożeniu macierzy i rozmazaniu Gaussa.
Najpierw pozwól mi wymienić jedną z funkcji, których używam do transpozycji ( EDYCJA: zobacz koniec mojej odpowiedzi, gdzie znalazłem znacznie szybsze rozwiązanie )
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];
}
}
Zobaczmy teraz, dlaczego transpozycja jest przydatna. Rozważ mnożenie macierzy C = A * B. Moglibyśmy to zrobić w ten sposó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*l+j];
}
C[K*i + j] = tmp;
}
}
W ten sposób będzie jednak dużo chybień w pamięci podręcznej. Znacznie szybszym rozwiązaniem jest wykonanie transpozycji 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);
Mnożenie macierzy to O (n ^ 3), a transpozycja to O (n ^ 2), więc wykonanie transpozycji powinno mieć znikomy wpływ na czas obliczeń (dla dużych n
). W przypadku pętli mnożenia macierzy jest jeszcze bardziej efektywne niż wykonanie transpozycji, ale jest to znacznie bardziej skomplikowane.
Żałuję, że nie znam szybszego sposobu wykonania transpozycji ( Edycja: znalazłem szybsze rozwiązanie, zobacz koniec mojej odpowiedzi ). Kiedy Haswell / AVX2 wyjdzie za kilka tygodni, będzie miał funkcję zbierającą. Nie wiem, czy to będzie pomocne w tym przypadku, ale mógłbym sobie wyobrazić zbieranie kolumny i pisanie wiersza. Może sprawi, że transpozycja stanie się niepotrzebna.
W przypadku rozmazywania Gaussa rozmazujesz w poziomie, a następnie w pionie. Ale smużenie w pionie ma problem z pamięcią podręczną, więc to, co robisz, jest
Smear image horizontally
transpose output
Smear output horizontally
transpose output
Oto dokument firmy Intel wyjaśniający, że
http://software.intel.com/en-us/articles/iir-gaussian-blur-filter-implementation-using-intel-advanced-vector-extensions
Wreszcie, to, co faktycznie robię w mnożeniu macierzy (i rozmazaniu Gaussa) nie polega na dokładnej transpozycji, ale na transpozycji w szerokościach o określonym rozmiarze wektora (np. 4 lub 8 dla SSE / AVX). Oto funkcja, której używam
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];
}
}
EDYTOWAĆ:
Wypróbowałem kilka funkcji, aby znaleźć najszybszą transpozycję dla dużych macierzy. Ostatecznie najszybszym rezultatem jest użycie blokowania pętli z block_size=16
( Edycja: znalazłem szybsze rozwiązanie wykorzystujące SSE i blokowanie pętli - patrz poniżej ). Ten kod działa dla dowolnej macierzy NxM (tj. Macierz nie musi być kwadratowa).
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);
}
}
}
Wartości lda
i ldb
są szerokością macierzy. Muszą to być wielokrotności rozmiaru bloku. Aby znaleźć wartości i przydzielić pamięć np. Dla macierzy 3000x1001, robię coś takiego
#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);
Dla 3000x1001 zwraca ldb = 3008
i lda = 1008
Edytować:
Znalazłem jeszcze szybsze rozwiązanie przy użyciu funkcji wewnętrznych 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);
}
}
}
}
}