it-swarm.com.de

Was ist der schnellste Weg, um eine Matrix in C ++ zu transponieren?

Ich habe eine Matrix (relativ groß), die ich transponieren muss. Angenommen, meine Matrix ist

a b c d e f
g h i j k l
m n o p q r 

Ich möchte, dass das Ergebnis wie folgt aussieht:

a g m
b h n
c I o
d j p
e k q
f l r

Wie geht das am schnellsten?

72
mans

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. bei Matrixmultiplikation und Gaußscher Verschmierung.

Lassen Sie mich zuerst eine der Funktionen auflisten, die ich für die Transponierung verwende ( EDIT: 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 werden jedoch viele Cache-Fehler auftreten. 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). In der Matrixmultiplikation ist Loop-Tiling sogar effektiver als die Transponierung, 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 ein paar Wochen herauskommt, wird es eine Gather-Funktion haben. Ich weiß nicht, ob das in diesem Fall hilfreich sein wird, aber ich könnte mir vorstellen, wie ich eine Spalte sammle und eine Zeile aufschreibe. Vielleicht macht es die Transponierung unnötig.

Beim Gaußschen Verschmieren wird horizontal und dann vertikal verschmiert. Aber vertikales Schmieren hat das Cache-Problem, also ist das, was Sie tun

Smear image horizontally
transpose output 
Smear output horizontally
transpose output

Hier ist ein Artikel von Intel, der erklärt, dass http://software.intel.com/de-de/articles/iir-gaussian-blur-filter-implementation- using-intel-advanced-vector-extensions

Was ich bei der Matrixmultiplikation (und beim Gaußschen Verschmieren) tatsächlich mache, ist 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];
    }
}

EDIT:

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 ( Edit: Ich habe eine schnellere Lösung mit SSE und Schleifenblockierung - siehe unten ) gefunden. 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 geben die Breite der Matrix an. Dies müssen Vielfache der Blockgröße sein. Um die Werte zu finden und den Speicher für z.B. Bei einer 3000x1001 Matrix mache ich sowas

#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 ergibt dies ldb = 3008 und lda = 1008

Bearbeiten:

Ich fand eine noch schnellere Lösung mit SSE intrinsics:

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);
                }
            }
        }
    }
}
118
user2088790

Dies hängt von Ihrer Anwendung ab, aber im Allgemeinen besteht der schnellste Weg, eine Matrix zu transponieren, darin, Ihre Koordinaten zu invertieren, wenn Sie nachschlagen, und dann müssen Sie tatsächlich keine Daten verschieben.

38
Shafik Yaghmour

Einige Details zur Transponierung von 4x4-Quadrat-Float-Matrizen (32-Bit-Integer-Matrizen) mit x86-Hardware. Es ist hilfreich, hier zu beginnen, um größere quadratische Matrizen wie 8x8 oder 16x16 zu transponieren.

_MM_TRANSPOSE4_PS(r0, r1, r2, r3) wird von verschiedenen Compilern unterschiedlich implementiert. GCC und ICC (ich habe Clang nicht überprüft) verwenden unpcklps, unpckhps, unpcklpd, unpckhpd, Während MSVC nur shufps verwendet. Wir können diese beiden Ansätze tatsächlich so miteinander kombinieren.

t0 = _mm_unpacklo_ps(r0, r1);
t1 = _mm_unpackhi_ps(r0, r1);
t2 = _mm_unpacklo_ps(r2, r3);
t3 = _mm_unpackhi_ps(r2, r3);

r0 = _mm_shuffle_ps(t0,t2, 0x44);
r1 = _mm_shuffle_ps(t0,t2, 0xEE);
r2 = _mm_shuffle_ps(t1,t3, 0x44);
r3 = _mm_shuffle_ps(t1,t3, 0xEE);

Eine interessante Beobachtung ist, dass zwei Shuffles auf diese Weise in ein Shuffle und zwei Blends (SSE4.1) umgewandelt werden können.

t0 = _mm_unpacklo_ps(r0, r1);
t1 = _mm_unpackhi_ps(r0, r1);
t2 = _mm_unpacklo_ps(r2, r3);
t3 = _mm_unpackhi_ps(r2, r3);

v  = _mm_shuffle_ps(t0,t2, 0x4E);
r0 = _mm_blend_ps(t0,v, 0xC);
r1 = _mm_blend_ps(t2,v, 0x3);
v  = _mm_shuffle_ps(t1,t3, 0x4E);
r2 = _mm_blend_ps(t1,v, 0xC);
r3 = _mm_blend_ps(t3,v, 0x3);

Dies wandelte effektiv 4 Shuffles in 2 Shuffles und 4 Blends um. Dies verwendet 2 weitere Anweisungen als die Implementierung von GCC, ICC und MSVC. Der Vorteil besteht darin, dass der Anschlussdruck reduziert wird, was unter bestimmten Umständen von Vorteil sein kann. Derzeit können alle Shuffles und Unpacks nur an einen bestimmten Port gesendet werden, wohingegen die Mischungen an einen von zwei verschiedenen Ports gesendet werden können.

Ich habe versucht, 8 Shuffles wie MSVC zu verwenden und diese in 4 Shuffles + 8 Blends umzuwandeln, aber es hat nicht funktioniert. Ich musste noch 4 Auspackungen verwenden.

Ich habe die gleiche Technik für eine 8x8-Float-Transponierte verwendet (siehe am Ende dieser Antwort). https://stackoverflow.com/a/25627536/2542702 . In dieser Antwort musste ich noch 8 Unpacks verwenden, aber ich habe die 8 Shuffles in 4 Shuffles und 8 Blends umgewandelt.

Für 32-Bit-Ganzzahlen gibt es nichts Vergleichbares wie shufps (außer für 128-Bit-Shuffles mit AVX512), daher kann es nur mit Unpacks implementiert werden, die meiner Meinung nach nicht in Blends konvertiert werden können (effizient). Mit AVX512 verhält sich vshufi32x4 Effektiv wie shufps, mit Ausnahme von 128-Bit-Lanes mit 4 Ganzzahlen anstelle von 32-Bit-Floats, so dass dieselbe Technik in einigen Fällen möglicherweise mit vshufi32x4 Durchgeführt werden kann. Mit Knights Landing sind Mischmischungen viermal langsamer (Durchsatz) als Mischungen.

5
Z boson
template <class T>
void transpose( std::vector< std::vector<T> > a,
std::vector< std::vector<T> > b,
int width, int height)
{
    for (int i = 0; i < width; i++)
    {
        for (int j = 0; j < height; j++)
        {
            b[j][i] = a[i][j];
        }
    }
} 
1
Rachel Gallen

transponieren ohne Overhead (Klasse nicht vollständig):

class Matrix{
   double *data; //suppose this will point to data
   double _get1(int i, int j){return data[i*M+j];} //used to access normally
   double _get2(int i, int j){return data[j*N+i];} //used when transposed

   public:
   int M, N; //dimensions
   double (*get_p)(int, int); //functor to access elements  
   Matrix(int _M,int _N):M(_M), N(_N){
     //allocate data
     get_p=&Matrix::_get1; // initialised with normal access 
     }

   double get(int i, int j){
     //there should be a way to directly use get_p to call. but i think even this
     //doesnt incur overhead because it is inline and the compiler should be intelligent
     //enough to remove the extra call
     return (this->*get_p)(i,j);
    }
   void transpose(){ //twice transpose gives the original
     if(get_p==&Matrix::get1) get_p=&Matrix::_get2;
     else get_p==&Matrix::_get1; 
     swap(M,N);
     }
}

kann so verwendet werden:

Matrix M(100,200);
double x=M.get(17,45);
M.transpose();
x=M.get(17,45); // = original M(45,17)

natürlich habe ich mich hier nicht um das Speichermanagement gekümmert, was ein entscheidendes, aber anderes Thema ist.

1
Reza Baram

Betrachten Sie jede Zeile als Spalte und jede Spalte als Zeile. Verwenden Sie j, i anstelle von i, j

demo: http://ideone.com/lvsxKZ

#include <iostream> 
using namespace std;

int main ()
{
    char A [3][3] =
    {
        { 'a', 'b', 'c' },
        { 'd', 'e', 'f' },
        { 'g', 'h', 'i' }
    };

    cout << "A = " << endl << endl;

    // print matrix A
    for (int i=0; i<3; i++)
    {
        for (int j=0; j<3; j++) cout << A[i][j];
        cout << endl;
    }

    cout << endl << "A transpose = " << endl << endl;

    // print A transpose
    for (int i=0; i<3; i++)
    {
        for (int j=0; j<3; j++) cout << A[j][i];
        cout << endl;
    }

    return 0;
}
1
Khaled.K

Wenn die Größe der Arrays vorab bekannt ist, können wir die Vereinigung als Hilfe verwenden. So was-

#include <bits/stdc++.h>
using namespace std;

union ua{
    int arr[2][3];
    int brr[3][2];
};

int main() {
    union ua uav;
    int karr[2][3] = {{1,2,3},{4,5,6}};
    memcpy(uav.arr,karr,sizeof(karr));
    for (int i=0;i<3;i++)
    {
        for (int j=0;j<2;j++)
            cout<<uav.brr[i][j]<<" ";
        cout<<'\n';
    }

    return 0;
}
0
Sandeep K V