簡體   English   中英

在 C++ 中轉置矩陣的最快方法是什么?

[英]What is the fastest way to transpose a matrix in C++?

我有一個需要轉置的矩陣(相對較大)。 例如假設我的矩陣是

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

我希望結果如下:

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

執行此操作的最快方法是什么?

這是一個很好的問題。 您想要在內存中實際轉置矩陣而不僅僅是交換坐標的原因有很多,例如在矩陣乘法和高斯拖尾中。

首先讓我列出我用於轉置的功能之一(編輯:請參閱我的答案的結尾,我找到了一個更快的解決方案

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

現在讓我們看看為什么轉置很有用。 考慮矩陣乘法 C = A*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;
    }
}

但是,這種方式將有很多緩存未命中。 一個更快的解決方案是先對 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);

矩陣乘法為 O(n^3),轉置為 O(n^2),因此采用轉置對計算時間的影響可以忽略不計(對於大n )。 在矩陣乘法循環中,平鋪甚至比轉置更有效,但這要復雜得多。

我希望我知道一種更快的轉置方法(編輯:我找到了一個更快的解決方案,請參閱我的答案結尾)。 當 Haswell/AVX2 幾周后出來時,它將具有聚集功能。 我不知道這在這種情況下是否會有所幫助,但我可以想象收集一列並寫出一行。 也許它會使轉置變得不必要。

對於高斯塗抹,您所做的是水平塗抹然后垂直塗抹。 但是垂直塗抹有緩存問題,所以你要做的是

Smear image horizontally
transpose output 
Smear output horizontally
transpose output

這是英特爾的一篇論文,解釋了http://software.intel.com/en-us/articles/iir-gaussian-blur-filter-implementation-using-intel-advanced-vector-extensions

最后,我在矩陣乘法(以及高斯拖尾)中實際做的不是完全采用轉置,而是采用特定矢量大小(例如,SSE/AVX 為 4 或 8)的寬度的轉置。 這是我使用的功能

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

編輯:

我嘗試了幾個函數來找到大矩陣的最快轉置。 最后,最快的結果是使用block_size=16循環阻塞(編輯:我找到了一個使用 SSE 和循環阻塞的更快的解決方案 - 見下文)。 此代碼適用於任何 NxM 矩陣(即矩陣不必是正方形)。

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

ldaldb是矩陣的寬度。 這些需要是塊大小的倍數。 為了找到值並為例如 3000x1001 矩陣分配內存,我做這樣的事情

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

對於 3000x1001,這將返回ldb = 3008lda = 1008

編輯:

我找到了一個使用 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);
                }
            }
        }
    }
}

這將取決於您的應用程序,但通常轉置矩陣的最快方法是在您查找時反轉您的坐標,然后您不必實際移動任何數據。

關於使用 x86 硬件轉置 4x4 方形浮點(我將在稍后討論 32 位整數)矩陣的一些細節。 從這里開始轉置較大的方陣(例如 8x8 或 16x16)很有幫助。

_MM_TRANSPOSE4_PS(r0, r1, r2, r3)由不同的編譯器以不同的方式實現。 GCC 和 ICC(我沒有檢查 Clang)使用unpcklps, unpckhps, unpcklpd, unpckhpd而 MSVC 只使用shufps 我們實際上可以像這樣將這兩種方法結合在一起。

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

一個有趣的觀察結果是,兩個 shuffle 可以像這樣轉換為一個 shuffle 和兩個混合 (SSE4.1)。

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

這有效地將 4 次 shuffle 轉換為 2 次 shuffle 和 4 次混合。 這比 GCC、ICC 和 MSVC 的實現多使用 2 條指令。 優點是它降低了端口壓力,這在某些情況下可能是有益的。 目前所有的洗牌和解包只能到一個特定的端口,而混合可以去兩個不同的端口中的任何一個。

我嘗試使用像 MSVC 這樣的 8 次 shuffle 並將其轉換為 4 次 shuffle + 8 混合,但沒有奏效。 我仍然不得不使用 4 個解包。

我對 8x8 浮點轉置使用了相同的技術(請參閱該答案的末尾)。 https://stackoverflow.com/a/25627536/2542702 在那個答案中,我仍然必須使用 8 次解包,但我設法將 8 次洗牌轉換為 4 次洗牌和 8 次混合。

對於 32 位整數,沒有什么比shufps (除了shufps的 128 位 shuffle 之外),所以它只能通過解包來實現,我認為它不能轉換為混合(有效)。 使用 AVX512, vshufi32x4作用類似於shufps除了 4 個整數的 128 位通道而不是 32 位浮點數,因此在某些情況下, vshufi32x4可能使用相同的技術。 使用 Knights Landing,shuffle 比 Blends 慢四倍(吞吐量)。

如果事先知道數組的大小,那么我們可以使用聯合來幫助我們。 像這樣-

#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;
}
template <class T>
void transpose( const 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];
        }
    }
} 

將每一行視為一列,將每一列視為一行 .. 使用 j,i 而不是 i,j

演示: 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;
}

轉置沒有任何開銷(類不完整):

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

可以這樣使用:

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

當然,我沒有打擾這里的內存管理,這是至關重要但不同的主題。

現代線性代數庫包括最常見運算的優化版本。 其中許多包括動態 CPU 調度,它在程序執行時為硬件選擇最佳實現(不影響可移植性)。

這通常是通過向量擴展內在函數手動優化您的函數的更好選擇。 后者會將您的實現與特定的硬件供應商和型號聯系起來:如果您決定更換到不同的供應商(例如 Power、ARM)或更新的向量擴展(例如 AVX512),您將需要再次重新實現它以充分利用它們。

例如,MKL 轉置包括 BLAS 擴展函數imatcopy 您也可以在其他實現中找到它,例如 OpenBLAS:

#include <mkl.h>

void transpose( float* a, int n, int m ) {
    const char row_major = 'R';
    const char transpose = 'T';
    const float alpha = 1.0f;
    mkl_simatcopy (row_major, transpose, n, m, alpha, a, n, n);
}

對於 C++ 項目,您可以使用 Armadillo C++:

#include <armadillo>

void transpose( arma::mat &matrix ) {
    arma::inplace_trans(matrix);
}

intel mkl 建議就地和非就地轉置/復制矩陣。 這是文檔鏈接 我建議嘗試異地實現,因為就地速度快十個,並且最新版本的 mkl 的文檔包含一些錯誤。

我認為最快速的方式不應該高於 O(n^2) 也這樣你可以只使用 O(1) 空間:
這樣做的方法是成對交換,因為當您轉置矩陣時,您要做的是: M[i][j]=M[j][i] ,因此將 M[i][j] 存儲在 temp 中,然后M[i][j]=M[j][i],最后一步:M[j][i]=temp。 這可以通過一次完成,所以它應該花費 O(n^2)

我的答案是 3x3 矩陣的轉置

 #include<iostream.h>

#include<math.h>


main()
{
int a[3][3];
int b[3];
cout<<"You must give us an array 3x3 and then we will give you Transposed it "<<endl;
for(int i=0;i<3;i++)
{
    for(int j=0;j<3;j++)
{
cout<<"Enter a["<<i<<"]["<<j<<"]: ";

cin>>a[i][j];

}

}
cout<<"Matrix you entered is :"<<endl;

 for (int e = 0 ; e < 3 ; e++ )

{
    for ( int f = 0 ; f < 3 ; f++ )

        cout << a[e][f] << "\t";


    cout << endl;

    }

 cout<<"\nTransposed of matrix you entered is :"<<endl;
 for (int c = 0 ; c < 3 ; c++ )
{
    for ( int d = 0 ; d < 3 ; d++ )
        cout << a[d][c] << "\t";

    cout << endl;
    }

return 0;
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM