転置する必要がある行列(比較的大きい)があります。たとえば、私の行列が
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
これを行う最速の方法は何ですか?
これはいい質問です。単に座標を交換するのではなく、実際にメモリ内で行列を転置したい理由はたくさんあります。行列の乗算とガウススミアリング。
まず、転置に使用する関数の1つをリストします(EDIT:はるかに高速な解決策を見つけた答えの最後を参照してください)
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
最後に、私が実際に行列乗算(およびガウススミアリング)で行うことは、正確な転置ではなく、特定のベクトルサイズの幅での転置(たとえば、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);
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);
}
}
}
}
}
これはアプリケーションに依存しますが、一般的に、マトリックスを転置する最速の方法は、ルックアップ時に座標を反転することであり、実際にデータを移動する必要はありません。
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];
}
}
}
オーバーヘッドなしで転置する(クラスは完全ではない):
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)
もちろん、ここではメモリ管理を気にしませんでした。これは重要ですが、異なるトピックです。
各行を列と見なし、各列を行と見なします.i、jの代わりにj、iを使用します
#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;
}
配列のサイズが事前にわかっている場合、ユニオンを使用してヘルプを作成できます。このような-
#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;
}