【问题标题】:What is the fastest way to transpose a matrix in C++?在 C++ 中转置矩阵的最快方法是什么?
【发布时间】:2013-05-20 04:37:08
【问题描述】:

我有一个需要转置的矩阵(相对较大)。例如假设我的矩阵是

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

最快的方法是什么?

【问题讨论】:

  • 这就是所谓的“转置”。旋转 90 度是一个完全不同的概念。
  • 而且最快的方法不是旋转它,而是在访问数组时简单地交换索引顺序。
  • 再快也得访问矩阵的所有元素。
  • @HighPerformanceMark:我想这取决于,如果您希望按行顺序重复访问矩阵,那么使用“转置”标志会对您造成很大打击。
  • 转置矩阵因其与内存缓存有关的问题而臭名昭著。如果您的数组足够大以至于转置的性能非常重要,并且您无法通过简单地提供带有交换索引的接口来避免转置,那么您最好的选择是使用现有的库例程来转置大型矩阵。专家已经做过这项工作,你应该使用它。

标签: c++ algorithm matrix transpose


【解决方案1】:

这是个好问题。有很多原因您希望在内存中实际转置矩阵,而不仅仅是交换坐标,例如在矩阵乘法和高斯拖尾。

首先让我列出我用于转置的一个函数(编辑:请参阅我的答案末尾,在那里我找到了一个更快的解决方案

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 = 3008 lda = 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);
                }
            }
        }
    }
}

【讨论】:

  • 不错的镜头,但我不确定'矩阵乘法是 O(n^3)',我认为它是 O(n^2)。
  • @ulyssis2 这是 O(n^3),除非你使用 Strassen 的矩阵乘法 (O(n^2.8074))。 user2088790:这做得很好。保存在我的个人收藏中。 :)
  • 以防万一,有人想知道是谁写了这个答案是我。我退出了一次,克服了它,然后回来了。
  • @ulyssis2 朴素矩阵乘法绝对是 O(n^3),据我所知,计算内核实现了朴素算法(我认为这是因为 Strassen 最终做了更多的操作(添加),如果你能做快速产品,这很糟糕,但我可能是错的)。矩阵乘法是否可以是 O(n^2) 是一个悬而未决的问题。
  • 请注意,如果行数和列数不是 4 的倍数,最后一个 SSE sn-p 将无法正常工作。它将使边框单元格保持不变。
【解决方案2】:

这将取决于您的应用程序,但一般来说,转置矩阵的最快方法是在您进行查找时反转您的坐标,然后您不必实际移动任何数据。

【讨论】:

  • 如果它是一个小矩阵或者你只从它读取一次,那就太好了。但是,如果转置矩阵很大并且需要多次重用,您可能仍然保存一个快速转置版本以获得更好的内存访问模式。 (+1,顺便说一句)
  • @Agentlien:为什么 A[j][i] 会比 A[i][j] 慢?
  • @beaker 如果你有一个大矩阵,不同的行/列可能会占用不同的缓存行/页。在这种情况下,您可能希望以这样一种方式迭代元素,以便一个接一个地访问相邻的元素。否则,它可能导致每个元素访问都成为缓存未命中,从而完全破坏性能。
  • @beaker:它与CPU级别的缓存有关(假设矩阵是一个大内存块),缓存行是矩阵的有效行,预取器可能会获取接下来的几行。如果您切换访问,CPU 缓存/预取器仍然会在您逐列访问时逐行工作,性能下降可能会非常剧烈。
  • @taocp 基本上,您需要某种标志来表明它已被转置,然后请求说(i,j) 将被映射到(j,i)
【解决方案3】:

关于用 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 和两个 blends (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 次洗牌转换为 2 次洗牌和 4 次混合。这比 GCC、ICC 和 MSVC 的实现多使用 2 条指令。优点是它降低了端口压力,这在某些情况下可能有好处。 目前所有的 shuffle 和 unpacks 只能到一个特定的端口,而 blends 可以到两个不同的端口中的任何一个。

我尝试使用 8 次随机播放(如 MSVC)并将其转换为 4 次随机播放 + 8 次混合,但没有成功。我仍然必须使用 4 个解包。

我对 8x8 浮点转置使用了相同的技术(请参阅该答案的末尾)。 https://stackoverflow.com/a/25627536/2542702。在那个答案中,我仍然必须使用 8 个解包,但我设法将 8 个 shuffle 转换为 4 个 shuffle 和 8 个 blends。

对于 32 位整数,没有什么像 shufps(除了 AVX512 的 128 位随机播放),所以它只能通过我认为不能转换为混合(有效)的解包来实现。使用 AVX512,vshufi32x4 的作用类似于shufps,除了 4 个整数的 128 位通道而不是 32 位浮点数,因此在某些情况下,vshufi32x4 可能使用相同的技术。在 Knights Landing 中,shuffle 的速度(吞吐量)是混合的四倍。

【讨论】:

  • 您可以在整数数据上使用shufps。如果您要进行大量改组,那么在 shufps + blendps 的 FP 域中进行所有操作可能是值得的,特别是如果您没有同样高效的 AVX2 vpblendd 可用。此外,在英特尔 SnB 系列硬件上,在像 paddd 这样的整数指令之间使用shufps 没有额外的旁路延迟。 (不过,根据 Agner Fog 的 SnB 测试,将 blendpspaddd 混合存在绕过延迟。)
  • @PeterCordes,我需要再次查看域更改。是否有一些表格(可能是关于 SO 的答案)总结了 Core2-Skylake 的域更改惩罚?无论如何,我对此进行了更多考虑。我现在明白为什么 wim 和你在我的 16x16 转置答案中一直提到 vinsertf64x4 而不是 vinserti64x4。如果我正在读取然后写入矩阵,那么使用浮点域或整数域当然没关系,因为转置只是移动数据。
  • Agner 的表格列出了 Core2 和 Nehalem(我认为是 AMD)的每个指令的域,但没有 SnB 系列。 Agner 的微架构指南只有一段说它在 SnB 上降至 1c,通常为 0,并提供了一些示例。英特尔的优化手册有一张我认为的表格,但我没有尝试过了解它,所以我不记得它有多少细节。我确实记得给定指令属于哪个类别并不完全清楚。
  • 即使您不只是写回内存,整个转置也只需 1 个额外的时钟。每个操作数的额外延迟可以并行(或交错方式)发生,因为转置的消费者开始读取由 shuffle 或 blends 写入的寄存器。乱序执行允许前几个 FMA 或其他什么在最后几个 shuffle 完成时开始,但是没有 dypass 延迟链,最多只是一个额外的延迟。
  • 好回答!英特尔 64-ia-32-architectures-optimization-manual 表 2-3 列出了 Skylake 的旁路延迟,您可能对此感兴趣。 Haswell 的表 2-8 看起来完全不同。
【解决方案4】:

如果事先知道数组的大小,那么我们可以使用联合来帮助我们。像这样-

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

【讨论】:

  • 我是 C/C++ 新手,但这看起来很天才。因为联合对其成员使用共享内存位置,所以您可以以不同的方式读取该内存。因此,您无需进行新的数组分配即可获得转置矩阵。我说的对吗?
【解决方案5】:

将每一行视为一列,将每一列视为一行.. 使用 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;
}

【讨论】:

    【解决方案6】:

    没有任何开销的转置(类不完整):

    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)
    

    当然,这里的内存管理我并没有打扰,这是至关重要但又不同的话题。

    【讨论】:

    • 每个元素访问都必须遵循函数指针的开销。
    【解决方案7】:

    现代线性代数库包括最常见运算的优化版本。其中许多包括动态 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);
    }
    

    【讨论】:

      【解决方案8】:

      intel mkl 建议就地和非就地转置/复制矩阵。这是link to the documentation。我建议尝试以更快的 10 就地实现不适当的实现,并且在最新版本的 mkl 的文档中包含一些错误。

      【讨论】:

        【解决方案9】:
        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];
                }
            }
        } 
        

        【讨论】:

        • 我宁愿认为如果交换两个循环会更快,因为写入时的缓存未命中惩罚比读取时要小。
        • 这仅适用于方阵。矩形矩阵是一个完全不同的问题!
        • 问题要求最快的方法。这只是一种方式。是什么让您认为它很快,更不用说最快了?对于大型矩阵,这会破坏缓存并且性能很差。
        • @NealB:你怎么理解的?
        • @EricPostpischil OP 询问的是一个相对较大的矩阵,所以我认为他们想“就地”这样做以避免分配双倍的内存。完成此操作后,源矩阵和目标矩阵的基地址相同。通过翻转行和列索引进行转置仅适用于方阵。有一些方法可以正确处理矩形矩阵,但它们有点复杂。
        【解决方案10】:

        我认为最快速的方法不应该高于 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)

        【讨论】:

        • M[i][j]=M[j][i] 只有当它是方阵时才有效;否则它会抛出一个索引异常。
        【解决方案11】:

        我的答案是 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;
        }
        

        【讨论】:

          猜你喜欢
          • 2016-02-06
          • 1970-01-01
          • 2013-03-05
          • 1970-01-01
          • 1970-01-01
          • 2012-11-30
          • 2020-09-02
          相关资源
          最近更新 更多