Tôi có một ma trận (tương đối lớn) mà tôi cần chuyển đổi. Ví dụ, giả sử rằng ma trận của tôi là
a b c d e f
g h i j k l
m n o p q r
Tôi muốn kết quả như sau:
a g m
b h n
c I o
d j p
e k q
f l r
Cách nhanh nhất để làm điều này là gì?
Tôi có một ma trận (tương đối lớn) mà tôi cần chuyển đổi. Ví dụ, giả sử rằng ma trận của tôi là
a b c d e f
g h i j k l
m n o p q r
Tôi muốn kết quả như sau:
a g m
b h n
c I o
d j p
e k q
f l r
Cách nhanh nhất để làm điều này là gì?
Câu trả lời:
Đây là một câu hỏi hay. Có nhiều lý do mà bạn muốn thực sự chuyển ma trận trong bộ nhớ thay vì chỉ hoán đổi tọa độ, ví dụ như trong phép nhân ma trận và bôi bẩn Gauss.
Trước tiên, hãy để tôi liệt kê một trong những hàm tôi sử dụng cho phép chuyển vị ( CHỈNH SỬA: vui lòng xem phần cuối câu trả lời của tôi, nơi tôi tìm thấy một giải pháp nhanh hơn nhiều )
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];
}
}
Bây giờ chúng ta hãy xem tại sao chuyển vị lại hữu ích. Xét phép nhân ma trận C = A * B. Chúng tôi có thể làm theo cách này.
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ằng cách đó, tuy nhiên, sẽ có rất nhiều bộ nhớ cache bị bỏ sót. Một giải pháp nhanh hơn nhiều là chuyển vị trí của B trước
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);
Phép nhân ma trận là O (n ^ 3) và phép chuyển vị là O (n ^ 2), vì vậy việc lấy phép chuyển vị sẽ ảnh hưởng không đáng kể đến thời gian tính toán (đối với lớn n
). Trong phép nhân ma trận, việc xếp gạch vòng lặp thậm chí còn hiệu quả hơn việc lấy phép chuyển vị nhưng điều đó phức tạp hơn nhiều.
Tôi ước tôi biết một cách nhanh hơn để thực hiện chuyển vị ( Chỉnh sửa: Tôi đã tìm thấy một giải pháp nhanh hơn, xem phần cuối của câu trả lời của tôi ). Khi Haswell / AVX2 ra mắt trong vài tuần tới, nó sẽ có chức năng tập hợp. Tôi không biết liệu điều đó có hữu ích trong trường hợp này hay không nhưng tôi có thể hình ảnh tập hợp một cột và viết ra một hàng. Có thể nó sẽ làm cho việc chuyển đổi không cần thiết.
Đối với cách bôi Gaussian, những gì bạn làm là bôi theo chiều ngang và sau đó bôi theo chiều dọc. Nhưng bôi bẩn theo chiều dọc có vấn đề về bộ nhớ cache, vì vậy những gì bạn làm là
Smear image horizontally
transpose output
Smear output horizontally
transpose output
Đây là một bài báo của Intel giải thích rằng http://software.intel.com/en-us/articles/iir-gaussian-blur-filter-implementation-using-intel-advanced-vector-extensions
Cuối cùng, những gì tôi thực sự làm trong phép nhân ma trận (và trong phép bôi Gaussian) không phải là lấy chính xác chuyển vị mà lấy chuyển vị theo độ rộng của một kích thước vectơ nhất định (ví dụ: 4 hoặc 8 cho SSE / AVX). Đây là chức năng tôi sử dụng
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];
}
}
BIÊN TẬP:
Tôi đã thử một số chức năng để tìm chuyển vị nhanh nhất cho ma trận lớn. Cuối cùng, kết quả nhanh nhất là sử dụng chặn vòng lặp với block_size=16
( Chỉnh sửa: Tôi đã tìm thấy một giải pháp nhanh hơn bằng cách sử dụng SSE và chặn vòng lặp - xem bên dưới ). Mã này hoạt động với bất kỳ ma trận NxM nào (tức là ma trận không phải là hình vuông).
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);
}
}
}
Các giá trị lda
và ldb
là chiều rộng của ma trận. Chúng phải là bội số của kích thước khối. Để tìm các giá trị và cấp phát bộ nhớ cho ma trận 3000x1001, tôi làm như sau
#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);
Đối với 3000x1001, nó trả về ldb = 3008
và lda = 1008
Biên tập:
Tôi đã tìm thấy một giải pháp thậm chí còn nhanh hơn bằng cách sử dụng bản chất 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ều này sẽ phụ thuộc vào ứng dụng của bạn nhưng nói chung cách nhanh nhất để chuyển ma trận là đảo ngược tọa độ của bạn khi bạn tra cứu, sau đó bạn thực sự không phải di chuyển bất kỳ dữ liệu nào.
(i,j)
sẽ được ánh xạ tới(j,i)
Một số chi tiết về chuyển đổi ma trận float vuông 4x4 (tôi sẽ thảo luận về số nguyên 32 bit sau) bằng phần cứng x86. Thật hữu ích khi bắt đầu ở đây để chuyển các ma trận vuông lớn hơn như 8x8 hoặc 16x16.
_MM_TRANSPOSE4_PS(r0, r1, r2, r3)
được thực hiện khác nhau bởi các trình biên dịch khác nhau. GCC và ICC (tôi chưa kiểm tra Clang) sử dụng unpcklps, unpckhps, unpcklpd, unpckhpd
trong khi MSVC chỉ sử dụng shufps
. Chúng ta thực sự có thể kết hợp hai cách tiếp cận này với nhau như thế này.
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);
Một quan sát thú vị là hai lần trộn có thể được chuyển đổi thành một lần trộn và hai lần trộn (SSE4.1) như thế này.
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);
Điều này đã chuyển đổi hiệu quả 4 lần trộn thành 2 lần trộn và 4 lần trộn. Điều này sử dụng nhiều hơn 2 hướng dẫn so với việc triển khai GCC, ICC và MSVC. Ưu điểm là nó làm giảm áp lực cổng có thể có lợi trong một số trường hợp. Hiện tại, tất cả các lần trộn và giải nén chỉ có thể đi đến một cổng cụ thể trong khi các bộ trộn có thể đi đến một trong hai cổng khác nhau.
Tôi đã thử sử dụng 8 trộn như MSVC và chuyển đổi đó thành 4 trộn + 8 trộn nhưng nó không hoạt động. Tôi vẫn phải sử dụng 4 lần giải nén.
Tôi đã sử dụng kỹ thuật tương tự cho phép chuyển vị phao 8x8 (xem ở cuối câu trả lời đó). https://stackoverflow.com/a/25627536/2542702 . Trong câu trả lời đó, tôi vẫn phải sử dụng 8 lần mở gói nhưng tôi đã chuyển đổi 8 lần trộn thành 4 lần trộn và 8 lần trộn.
Đối với số nguyên 32-bit thì không có gì giống như vậy shufps
(ngoại trừ trộn 128-bit với AVX512) vì vậy nó chỉ có thể được thực hiện với các gói giải nén mà tôi không nghĩ có thể chuyển đổi thành hỗn hợp (hiệu quả). Với AVX512 vshufi32x4
hoạt động hiệu quả như shufps
ngoại trừ các làn 128-bit gồm 4 số nguyên thay vì số nổi 32-bit, vì vậy kỹ thuật tương tự này có thể áp dụng vshufi32x4
trong một số trường hợp. Với Knights Landing xáo trộn chậm hơn bốn lần (thông lượng) so với hỗn hợp.
shufps
trên dữ liệu số nguyên. Nếu bạn đang thực hiện nhiều lần xáo trộn, bạn có thể thực hiện tất cả trong miền FP cho shufps
+ blendps
, đặc biệt nếu bạn không có vpblendd
sẵn AVX2 hiệu quả như nhau . Ngoài ra, trên phần cứng Intel SnB-family, không có thêm độ trễ bỏ qua để sử dụng shufps
giữa các lệnh số nguyên như paddd
. (Tuy nhiên, có một độ trễ bỏ qua để trộn blendps
với paddd
, theo thử nghiệm SnB của Agner Fog.)
vinsertf64x4
trong câu trả lời chuyển vị 16x16 của tôi thay vì vinserti64x4
. Nếu tôi đang đọc sau đó viết ma trận thì chắc chắn không thành vấn đề nếu tôi sử dụng miền dấu phẩy động hay miền số nguyên vì phép chuyển vị chỉ là dữ liệu di chuyển.
Hãy coi mỗi hàng là một cột và mỗi cột là một hàng .. sử dụng j, i thay vì i, j
demo: 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;
}
chuyển vị mà không có bất kỳ chi phí nào (lớp không hoàn thành):
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);
}
}
có thể được sử dụng như thế này:
Matrix M(100,200);
double x=M.get(17,45);
M.transpose();
x=M.get(17,45); // = original M(45,17)
tất nhiên tôi không bận tâm đến việc quản lý bộ nhớ ở đây, đây là một chủ đề quan trọng nhưng khác.
Nếu kích thước của các mảng đã biết trước thì chúng ta có thể sử dụng union để trợ giúp. Như thế này-
#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];
}
}
}
Các thư viện đại số tuyến tính hiện đại bao gồm các phiên bản được tối ưu hóa của các phép toán phổ biến nhất. Nhiều người trong số họ bao gồm điều phối CPU động, chọn cách triển khai tốt nhất cho phần cứng tại thời điểm thực thi chương trình (mà không ảnh hưởng đến tính di động).
Đây thường là một giải pháp thay thế tốt hơn để thực hiện tối ưu hóa thủ công các hàm của bạn thông qua các chức năng nội tại của phần mở rộng vectơ. Cái sau sẽ ràng buộc việc triển khai của bạn với một nhà cung cấp phần cứng và mô hình cụ thể: nếu bạn quyết định hoán đổi sang một nhà cung cấp khác (ví dụ: Power, ARM) hoặc sang một phần mở rộng vectơ mới hơn (ví dụ: AVX512), bạn sẽ cần triển khai lại nó để tận dụng tối đa chúng.
Ví dụ, chuyển vị MKL bao gồm chức năng mở rộng BLAS imatcopy
. Bạn cũng có thể tìm thấy nó trong các triển khai khác như 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);
}
Đối với một dự án C ++, bạn có thể sử dụng Armadillo C ++:
#include <armadillo>
void transpose( arma::mat &matrix ) {
arma::inplace_trans(matrix);
}
intel mkl đề xuất các ma trận chuyển vị / sao chép tại chỗ và ngoài vị trí. đây là liên kết đến tài liệu . Tôi khuyên bạn nên thử triển khai tại chỗ nhanh hơn mười tại chỗ và trong tài liệu của phiên bản mkl mới nhất có một số lỗi.
Tôi nghĩ rằng cách nhanh nhất không nên lấy cao hơn O (n ^ 2) cũng theo cách này, bạn có thể chỉ sử dụng khoảng trắng O (1):
cách để làm điều đó là hoán đổi theo từng cặp vì khi bạn hoán vị một ma trận thì bạn làm là: M [i] [j] = M [j] [i], do đó, lưu trữ M [i] [j] trong nhiệt độ, sau đó M [i] [j] = M [j] [i], và bước cuối cùng: M [j] [i] = temp. điều này có thể được thực hiện bởi một lần vượt qua, vì vậy nó sẽ mất O (n ^ 2)
câu trả lời của tôi là chuyển thành ma trận 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;
}