web-dev-qa-db-fra.com

Quel est le moyen le plus rapide de transposer une matrice en C ++?

J'ai une matrice (relativement grosse) que je dois transposer. Par exemple, supposons que ma matrice soit

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

Je veux que le résultat soit comme suit:

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

Quel est le moyen le plus rapide de faire cela?

72
mans

C'est une bonne question. Il existe de nombreuses raisons pour lesquelles vous souhaitez réellement transposer la matrice en mémoire plutôt que de simplement échanger des coordonnées, par exemple. dans la multiplication de matrice et le frottis gaussien.

Tout d’abord, laissez-moi énumérer l’une des fonctions que j’utilise pour la transposition ( EDIT: veuillez vous reporter à la fin de ma réponse, où j’ai trouvé une solution beaucoup plus rapide ).

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];
    }
}

Voyons maintenant pourquoi la transposition est utile. Considérons la multiplication matricielle C = A * B. Nous pourrions le faire de cette façon.

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 cette façon, il manquera beaucoup de cache. Une solution beaucoup plus rapide consiste à prendre d'abord la transposition 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 multiplication de matrice est O (n ^ 3) et la transposition est O (n ^ 2), donc prendre la transposition devrait avoir un effet négligeable sur le temps de calcul (pour les grandes n). Dans la matrice de multiplication, la mosaïque est encore plus efficace que la transposition, mais c'est beaucoup plus compliqué.

J'aimerais connaître un moyen plus rapide de transposer ( Edit: J'ai trouvé une solution plus rapide, voir la fin de ma réponse ). Lorsque Haswell/AVX2 sortira dans quelques semaines, il aura une fonction de collecte. Je ne sais pas si cela sera utile dans ce cas, mais je pourrais créer une colonne et en écrire une ligne. Cela rendra peut-être la transposition inutile.

Pour le gaussien, ce que vous faites est un frottis horizontal puis un frottis vertical. Mais le frottis vertical a le problème du cache donc ce que vous faites est

Smear image horizontally
transpose output 
Smear output horizontally
transpose output

Voici un article d'Intel expliquant que http://software.intel.com/en-us/articles/iir-gaussian-blur-filter-implementation-using-intel-advanced-vector-extensions

Enfin, ce que je fais réellement dans la multiplication matricielle (et dans le frottis gaussien) n’est pas exactement la transposition mais la transpose en largeur d’une certaine taille de vecteur (par exemple 4 ou 8 pour SSE/AVX). Voici la fonction que j'utilise

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:

J'ai essayé plusieurs fonctions pour trouver la transposition la plus rapide pour les grandes matrices. Au final, le résultat le plus rapide consiste à utiliser le blocage de boucle avec block_size=16 ( Edit: J'ai trouvé une solution plus rapide en utilisant SSE et le blocage de boucle - voir ci-dessous ). Ce code fonctionne pour toute matrice NxM (c'est-à-dire que la matrice ne doit pas nécessairement être carrée).

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

Les valeurs lda et ldb sont la largeur de la matrice. Celles-ci doivent être des multiples de la taille du bloc. Pour rechercher les valeurs et allouer la mémoire, par exemple, une matrice 3000x1001 je fais quelque chose comme ça

#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);

Pour 3000x1001, cela retourne ldb = 3008 et lda = 1008

Modifier:

J'ai trouvé une solution encore plus rapide en utilisant 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

Cela dépendra de votre application, mais en général, le moyen le plus rapide de transposer une matrice consiste à inverser vos coordonnées lorsque vous effectuez une recherche, sans avoir à déplacer les données.

38
Shafik Yaghmour

Quelques détails sur la transposition de matrices carrées float 4x4 (je traiterai plus tard de l'entier 32 bits) avec du matériel x86. Il est utile de commencer ici pour transposer des matrices plus grandes, telles que 8x8 ou 16x16.

_MM_TRANSPOSE4_PS(r0, r1, r2, r3) est implémenté différemment par différents compilateurs. GCC et ICC (je n'ai pas vérifié Clang) utilisent unpcklps, unpckhps, unpcklpd, unpckhpd alors que MSVC utilise uniquement shufps. Nous pouvons réellement combiner ces deux approches ensemble comme ceci.

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);

Une observation intéressante est que deux mélanges peuvent être convertis en un mélange et deux mélanges (SSE4.1) comme celui-ci.

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);

Cela convertit efficacement 4 mélanges en 2 mélanges et 4 mélanges. Ceci utilise 2 instructions de plus que l'implémentation de GCC, ICC et MSVC. L'avantage est que cela réduit la pression du port, ce qui peut présenter un avantage dans certaines circonstances. Actuellement, tous les shuffles et les unpacks ne peuvent aller qu’à un seul port, tandis que les mélanges peuvent aller à un ou deux ports différents.

J'ai essayé d'utiliser 8 mélanges comme MSVC et de le convertir en 4 mélanges + 8 mélanges, mais cela n'a pas fonctionné. Je devais encore utiliser 4 unpacks.

J'ai utilisé cette même technique pour une transposition 8x8 float (voir vers la fin de cette réponse). https://stackoverflow.com/a/25627536/2542702 . Dans cette réponse, il me restait encore à utiliser 8 déballages, mais j'ai réussi à convertir les 8 shuffles en 4 shuffles et 8 mélanges.

Pour les entiers 32 bits, il n'y a rien de tel que shufps (à l'exception des shuffles 128 bits avec AVX512), de sorte qu'il ne peut être implémenté qu'avec des unpacks qui, à mon avis, ne peuvent pas être convertis en dégradés (de manière efficace). Avec AVX512 vshufi32x4 agit efficacement comme shufps sauf pour les lignes de 4 bits à 128 bits au lieu des nombres flottants de 32 bits, de sorte que cette même technique pourrait éventuellement être utilisée avec vshufi32x4 dans certains cas. Avec les Knights Landing, les shuffles sont quatre fois plus lents (débit) que les mélanges.

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

transposition sans surcharge (classe non terminée):

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

peut être utilisé comme ceci:

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

bien entendu, je ne me suis pas préoccupé de la gestion de la mémoire, sujet crucial mais différent.

1
Reza Baram

Considérez chaque ligne comme une colonne et chaque colonne comme une ligne .. utilisez j, i au lieu de i, j

démo: 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

Si la taille des tableaux est connue auparavant, nous pourrions utiliser le syndicat à notre aide. Comme ça-

#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