我有一个(相对较大的)矩阵需要转置。例如,假设我的矩阵是
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
什么方法是最快的?
我有一个(相对较大的)矩阵需要转置。例如,假设我的矩阵是
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];
}
}
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在几周后推出时,它将具有gather函数。我不知道这在这种情况下是否有用,但我可以想象收集一列并写出一行。也许这会使转置变得不必要。
对于高斯模糊,您需要先进行水平模糊,然后进行垂直模糊。但是,垂直模糊存在缓存问题,因此您需要进行以下操作:
Smear image horizontally
transpose output
Smear output horizontally
transpose output
这里有一篇由英特尔撰写的论文,解释了使用英特尔高级向量扩展实现IIR高斯模糊滤波器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);
}
}
}
lda
和ldb
的值是矩阵的宽度。这些值需要是块大小的倍数。为了找到这些值并为例如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);
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);
}
}
}
}
}
这将取决于您的应用程序,但通常转置矩阵的最快方式是在查找时反转坐标,然后您不必实际移动任何数据。
(i,j)
将被映射到 (j,i)
。 - Shafik Yaghmour关于使用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个指令。优点是减少了端口压力,在某些情况下可能有好处。 目前所有的洗牌和解包都只能发送到一个特定的端口,而混合可以发送到两个不同的端口之一。
我尝试使用8个洗牌,就像MSVC那样,并将其转换为4个洗牌+8个混合,但没有成功。我仍然需要使用4个解包。
我在一个8x8浮点数转置中使用了相同的技术(请参阅答案末尾)。 https://dev59.com/cV8e5IYBdhLWcg3wja_5#25627536。在那个答案中,我仍然需要使用8个解包,但我设法将8个洗牌转换为4个洗牌和8个混合。
对于32位整数,除了AVX512的128位洗牌之外,没有像shufps
这样的操作码,所以只能用解包来实现,但我认为无法高效地将其转换为混合。使用AVX512 vshufi32x4
可以有效地执行与shufps
类似的操作,但它是针对4个整数的128位通道而不是32位浮点数,因此在某些情况下可能可以使用这种技术。在骑士着陆芯片上,洗牌的吞吐量比混合慢四倍。
shufps
。如果你要进行大量的洗牌操作,最好使用 FP 领域中的 shufps
+ blendps
进行全部操作,特别是当你没有同样高效的 AVX2 vpblendd
可用时。此外,在英特尔 SnB 系列硬件上,使用 shufps
在整数指令(如 paddd
)之间没有额外的旁路延迟。(根据 Agner Fog 的 SnB 测试,使用 blendps
与 paddd
混合会有旁路延迟。) - Peter Cordesvinsertf64x4
而不是vinserti64x4
。如果我读取然后写入矩阵,那么使用浮点域或整数域确实没有关系,因为转置只是移动数据。 - Z bosonvinsertf64x4
和vinserti64x4
是可以互换的。我没有理由提到其中一个或另一个。我只是在考虑64x4位数据。 - wim如果数组的大小是已知的,那么我们可以使用union来帮助我们。像这样-
#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;
}
#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);
}
}
can be used like this:
Matrix M(100,200);
double x=M.get(17,45);
M.transpose();
x=M.get(17,45); // = original M(45,17)
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];
}
}
}
现代线性代数库包括最常见操作的优化版本。其中许多包括动态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);
}
multiply:
for all sub_matrices in mat1 row
for all sub_matrices in mat2 column
select sub_matrix1
select sub_matrix2
if sub_mat2 is not transposed
transpose sub_mat2
multiply sub_mat1 and sub_mat2 <---- data in cache!
accumulate result
优势: