Das ist eine gute Frage. Es gibt viele Gründe, warum Sie die Matrix tatsächlich im Speicher transponieren möchten, anstatt nur die Koordinaten zu tauschen, z. B. bei der Matrixmultiplikation und beim Gaußschen Verschmieren.
Lassen Sie mich zunächst eine der Funktionen auflisten, die ich für die Transponierung verwende ( BEARBEITEN: Bitte lesen Sie das Ende meiner Antwort, wo ich eine viel schnellere Lösung gefunden habe ).
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];
}
}
Nun wollen wir sehen, warum die Transponierung nützlich ist. Betrachten Sie die Matrixmultiplikation C = A * B. Wir könnten es so machen.
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;
}
}
Auf diese Weise wird es jedoch viele Cache-Fehler geben. Eine viel schnellere Lösung besteht darin, zuerst die Transponierte von B zu nehmen
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);
Die Matrixmultiplikation ist O (n ^ 3) und die Transponierung ist O (n ^ 2), daher sollte die Transponierung einen vernachlässigbaren Einfluss auf die Rechenzeit haben (für große n
). Bei der Matrixmultiplikationsschleife ist das Kacheln noch effektiver als das Transponieren, aber das ist viel komplizierter.
Ich wünschte, ich wüsste einen schnelleren Weg, um die Transponierung durchzuführen ( Bearbeiten: Ich habe eine schnellere Lösung gefunden, siehe das Ende meiner Antwort ). Wenn Haswell / AVX2 in einigen Wochen herauskommt, hat es eine Sammelfunktion. Ich weiß nicht, ob das in diesem Fall hilfreich sein wird, aber ich könnte mir vorstellen, eine Spalte zu sammeln und eine Zeile zu schreiben. Vielleicht macht es die Transponierung unnötig.
Beim Gaußschen Verschmieren verschmieren Sie horizontal und dann vertikal. Vertikales Verschmieren hat jedoch das Cache-Problem. Sie tun dies also
Smear image horizontally
transpose output
Smear output horizontally
transpose output
Hier ist ein Artikel von Intel, der erklärt, dass
http://software.intel.com/en-us/articles/iir-gaussian-blur-filter-implementation-using-intel-advanced-vector-extensions
Schließlich mache ich bei der Matrixmultiplikation (und beim Gaußschen Verschmieren) nicht genau die Transponierung, sondern die Transponierung in Breiten einer bestimmten Vektorgröße (z. B. 4 oder 8 für SSE / AVX). Hier ist die Funktion, die ich benutze
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];
}
}
BEARBEITEN:
Ich habe mehrere Funktionen ausprobiert, um die schnellste Transponierung für große Matrizen zu finden. Am Ende ist das schnellste Ergebnis die Verwendung der Schleifenblockierung mit block_size=16
( Bearbeiten: Ich habe eine schnellere Lösung mit SSE und Schleifenblockierung gefunden - siehe unten ). Dieser Code funktioniert für jede NxM-Matrix (dh die Matrix muss nicht quadratisch sein).
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);
}
}
}
Die Werte lda
und ldb
sind die Breite der Matrix. Dies müssen Vielfache der Blockgröße sein. Um die Werte zu finden und den Speicher für zB eine 3000x1001-Matrix zuzuweisen, mache ich so etwas
#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);
Für 3000x1001 gibt dies ldb = 3008
und zurück lda = 1008
Bearbeiten:
Ich habe mit SSE Intrinsics eine noch schnellere Lösung gefunden:
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);
}
}
}
}
}