What is the fastest way to transpose a matrix in C++?
我有一个矩阵(相对较大),需要转置。例如,假设我的矩阵是
1 2 3 | a b c d e f g h i j k l m n o p q r |
我希望结果如下:
1 2 3 4 5 6 | a g m b h n c I o d j p e k q f l r |
最快的方法是什么?
这是个好问题。有很多原因需要在内存中实际转置矩阵,而不仅仅是交换坐标,例如在矩阵乘法和高斯涂抹中。
首先,让我列出一个用于转置的函数(编辑:请参阅我的答案的结尾,在这里我找到了一个更快的解决方案)
1 2 3 4 5 6 7 8 | 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,我们可以这样做。
1 2 3 4 5 6 7 8 9 | 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的转置
1 2 3 4 5 6 7 8 9 10 11 | 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),因此使用转置对计算时间的影响应该可以忽略不计(对于大型
我希望我知道一个更快的转置方法(编辑:我找到了一个更快的解决方案,见我的答案的结尾)。当Haswell/AVx2在几周后推出时,它将具有收集功能。我不知道在这种情况下这是否有帮助,但我可以想象收集一列并写出一行。也许这会使转置变得不必要。
对于高斯涂抹,您要做的是水平涂抹,然后垂直涂抹。但是垂直涂抹有缓存问题,所以您要做的是
1 2 3 4 | 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)进行转置。这是我使用的功能
1 2 3 4 5 6 7 8 9 | 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]; } } |
编辑:
我尝试了几个函数来寻找大型矩阵的最快转置。最后,最快的结果是将循环阻塞与
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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); } } } |
值
1 2 3 4 5 6 7 8 | #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); |
对于30000x1001,返回
编辑:
我发现了一个更快速的解决方案,使用SSE内部函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | 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。
1 2 3 4 5 6 7 8 9 | 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),就像这样。
1 2 3 4 5 6 7 8 9 10 11 | 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条指令。其优点是降低了端口压力,在某些情况下可能会有好处。目前,所有的无序排列和解包只能转到一个特定的端口,而混合可以转到两个不同的端口中的任何一个。
我试着用8个洗牌,像msvc,把它转换成4个洗牌+8个混合,但没有用。我还得用4个行李箱。
我对8x8的浮点转置使用了同样的技术(见答案的最后部分)。https://stackoverflow.com/a/25627536/2542702。在这个答案中,我仍然需要使用8个解包,但是我把8个解包转换成4个解包和8个混合解包。
对于32位整数,没有类似于
没有任何开销的转置(类不完整):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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); } } |
可以这样使用:
1 2 3 4 | Matrix M(100,200); double x=M.get(17,45); M.transpose(); x=M.get(17,45); // = original M(45,17) |
当然,我没有在这里讨论内存管理,这是非常重要的,但主题不同。
将每一行视为一列,每一列视为一行。用j,i代替i,j
演示:http://ideone.com/lvsxkz
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | #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 2 3 4 5 6 7 8 9 10 11 12 13 | 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]; } } } |
我认为最快的方法不应超过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矩阵的转置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | #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<<" Transposed 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; } |