用C ++转置矩阵最快的方法是什么?


80

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

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

最快的方法是什么?


2
这就是所谓的“移调”。旋转90度是一个完全不同的概念。
Andy Prowl

33
最快的方法不是旋转它,而是在访问数组时简单地交换索引顺序。
高性能标记

2
不管速度有多快,您都必须访问矩阵的所有元素。
taocp

10
@HighPerformanceMark:我想这取决于,如果您然后希望按行顺序重复访问矩阵,则具有“已转置”标志会给您带来沉重打击。
Matthieu M.

3
矩阵转置因其导致内存缓存的问题而臭名昭著。如果您的数组足够大以至于转置的性能显着,并且仅通过提供具有交换索引的接口就无法避免转置,那么最好的选择是使用现有的库例程来转置大型矩阵。专家已经完成了这项工作,您应该使用它。
埃里克·波斯特皮希尔

Answers:


129

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

首先,让我列出我用于移调的功能之一(编辑:请在我的答案结尾处找到更快的解决方案

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

这是Intel发表的一篇论文,解释了 http://software.intel.com/zh-CN/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);
                }
            }
        }
    }
}

1
拍摄不错,但我不确定“矩阵乘法是O(n ^ 3)”,我认为它是O(n ^ 2)。
ulyssis2 '12

2
@ ulyssis2它是O(n ^ 3),除非您使用Strassen的矩阵乘法(O(n ^ 2.8074))。user2088790:做得很好。将此保存在我的个人收藏中。:)
saurabheights

10
万一有人想知道谁写了这个答案,那是我。我一次退出了,克服了,然后回来了。
Z玻色子

1
@ ulyssis2朴素矩阵乘法绝对是O(n ^ 3),据我所知,计算内核实现了朴素算法(我认为这是因为Strassen最终会执行更多的操作(加法),如果您可以生产快速产品,但我可能错了。矩阵乘法是否可以为O(n ^ 2)是一个悬而未决的问题。
étale-上同调

通常,依靠线性代数库为您完成工作是更好的选择。诸如Intel MKL,OpenBLAS等的现代库提供了动态CPU调度,该调度为您的硬件选择了最佳的实现方式(例如,比SSE可用的向量寄存器更宽的向量寄存器:AVX AVX2,AVX512 ...),所以您不必无需制作非便携式程序即可获得快速程序。
豪尔赫·贝隆19'Sep

39

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


32
如果它是一个小的矩阵,或者您仅从中读取一次,则非常好。但是,如果转置矩阵很大并且需要多次重用,则可能仍要保存一个快速的转置版本以获得更好的内存访问模式。(+1,btw)
Agentlien

2
@Agentlien:为什么A [j] [i]比A [i] [j]慢?
烧杯

31
@beaker如果矩阵很大,则不同的行/列可能会占用不同的缓存行/页。在这种情况下,您希望以一种互相访问相邻元素的方式来迭代元素。否则,它可能导致每个元素访问都变成高速缓存未命中,从而完全破坏性能。
Agentlien

10
@beaker:这与在CPU级别进行缓存有关(假设矩阵是一个大的内存块),然后缓存行就是矩阵的有效行,预取器可以提取接下来的几行。如果切换访问,则在逐列访问时,CPU高速缓存/预取器仍将逐行工作,性能下降可能会很大。
Matthieu M.

2
@taocp基本上,您将需要某种标记来指示它已被转置,然后(i,j)将要说的请求映射到(j,i)
Shafik Yaghmour

5

有关使用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);

一个有趣的发现是,可以将两种混洗转换为一种混洗和两种混合(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条指令。优点是它可以降低端口压力,这在某些情况下可能会有所帮助。当前,所有混洗和拆包只能进入一个特定的港口,而混合酒可以进入两个不同的港口之一。

我尝试使用MSVC等8种混洗,并将其转换为4种混洗+ 8种混合,但没有用。我仍然必须使用4个解压缩包。

我对8x8浮点转置使用了相同的技术(请参见该答案的结尾)。 https://stackoverflow.com/a/25627536/2542702。在那个答案中,我仍然必须使用8个解包程序,但是我设法将8个混洗转换为4个混洗和8种混合处理。

对于32位整数,没有类似的东西shufps(AVX512的128位随机播放除外),因此只能通过解压缩来实现,我认为这些解压缩不能(有效地)转换为混合。使用AVX512时,其vshufi32x4行为类似于shufps4个整数的128位通道而不是32位浮点数,因此vshufi32x4在某些情况下可能也可以使用相同的技术。使用Knights Landing,混洗的速度(吞吐量)比混合慢4倍。


1
您可以使用shufps整数数据。如果您要进行大量改组,那么在FP域中为shufps+进行全部操作可能是值得的blendps,尤其是在您没有同样有效的AVX2的vpblendd情况下。另外,在Intel SnB系列硬件上,shufps在整数指令之间使用时没有多余的旁路延迟paddd。(不过,根据Agner Fog的SnB测试,blendps与混合时会有旁路延迟paddd。)
Peter Cordes

@PeterCordes,我需要再次查看域更改。是否有一些表格(也许是SO的答案)总结了Core2-Skylake的域更改代价?无论如何,我对此已经作了更多的考虑。现在我知道为什么wim和您vinsertf64x4在我的16x16转置答案中一直提到而不是vinserti64x4。如果我正在阅读然后编写矩阵,那么我使用浮点域还是整数域都没关系,因为转置只是在移动数据。
Z玻色子

1
Agner的表格列出了Core2和Nehalem(以及我认为的AMD)每条指令的域,但没有列出SnB系列。Agner的微体系结构指南只有一段话说,在SnB上它降至1c,通常为0,并带有一些示例。我认为英特尔的优化手册有一张表格,但我没有尝试过使用它,所以我不记得它有多少细节。我不记得它不是完全明显的是什么类别的指定指令将会英寸
彼得·科德斯

即使您不只是写回内存,整个转置也只需要1个额外的时钟。当转置的使用者开始读取混洗或混合写入的寄存器时,每个操作数的额外延迟可能并行(或交错)发生。乱序执行允许前几个FMA或其他任何东西在最后几个改组完成时开始,但是没有绕行延迟,最多只有一个额外的延迟。
彼得·科德斯

1
尼克回答!表2-3中的intel 64-ia-32-architectures-optimization-manual列出了Skylake的旁路延迟,也许您对此很感兴趣。Haswell的表2-8看起来完全不同。
2013年

1

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

1

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

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)

当然,我在这里没有理会内存管理,这很重要,但主题却不同。


4
您的函数指针有一个开销,每个元素访问都必须遵循这些开销。
user877329 2014年

1

如果事先知道数组的大小,则可以使用联合来提供帮助。像这样-

#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 ++的新手,但这看起来很天才。由于union为其成员使用共享内存位置,因此可以不同地读取该内存。因此,您无需进行新的数组分配即可获得转置矩阵。我对吗?
Douguş16

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

1
我宁愿认为如果交换两个循环会更快,因为写入时的缓存未命中损失小于读取时的损失。
phoeagon 2013年

5
这仅适用于方矩阵。矩形矩阵是一个完全不同的问题!
NealB

2
问题要求最快的方法。这只是一种方法。是什么让您认为它很快,更不用说最快了?对于大型矩阵,这将破坏高速缓存并具有糟糕的性能。
埃里克·波斯特皮希尔

1
@NealB:你怎么看?
埃里克·波斯特皮希尔

@EricPostpischil OP正在询问一个相对较大的矩阵,因此我认为他们想“就地”执行此操作,以避免分配双倍的内存。完成此操作后,源矩阵和目标矩阵的基地址相同。通过翻转行和列索引进行转置仅适用于正方形矩阵。有一些方法可以使矩形矩阵正确,但是它们有些复杂。
NealB 2013年

0

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

0

intel mkl建议就地和就地换位/复制矩阵。这是文档链接。我建议尝试非常规实现,因为十个就地实现更快,并且在最新版本的mkl的文档中包含一些错误。


-1

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


2
M [i] [j] = M [j] [i]仅在为方矩阵时有效;否则会抛出索引异常。
安东尼·托马斯

-6

我的答案是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;
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.