C ++で行列を転置する最速の方法は何ですか?


81

転置する必要のある行列(比較的大きい)があります。たとえば、私の行列が

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度回転することは、まったく異なる概念です。
アンディプロール2013年

35
そして、最速の方法は、それを回転させるのではなく、配列にアクセスするときに単にインデックスの順序を入れ替えることです。
高性能マーク

2
どんなに速くても、とにかくマトリックスのすべての要素にアクセスする必要があります。
taocp 2013年

10
@HighPerformanceMark:マトリックスに行順に繰り返しアクセスしたい場合は、「転置」フラグを設定すると大きな打撃を受けると思います。
Matthieu M. 2013

3
行列の転置は、メモリキャッシュで発生する問題で有名です。配列が十分に大きく、転置のパフォーマンスが重要であり、インデックスを交換したインターフェイスを提供するだけでは転置を回避できない場合は、既存のライブラリルーチンを使用して大きな行列を転置するのが最善の方法です。専門家はすでにこの作業を行っているので、それを使用する必要があります。
Eric Postpischil 2013年

回答:


131

これは良い質問です。行列の乗算やガウススミアリングなど、座標を交換するだけでなく、実際に行列をメモリ内で転置したい理由はたくさんあります。

まず、転置に使用する関数の1つをリストします(編集:はるかに高速な解決策を見つけた私の回答の最後を参照してください

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

これは、http://software.intel.com/en-us/articles/iir-gaussian-blur-filter-implementation-using-intel-advanced-vector-extensionsについて説明しているIntelの論文です。

最後に、行列乗算(およびガウススミアリング)で実際に行うことは、転置を正確に行うのではなく、特定のベクトルサイズ(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 2016

2
@ ulyssis2 Strassenの行列乗算(O(n ^ 2.8074))を使用しない限り、O(n ^ 3)です。user2088790:これは非常によくできています。これを私の個人的なコレクションに保管します。:)
saurabheights 2016

10
誰かがこの答えを書いたのは私だったのか知りたがっています。私は一度SOをやめて、それを乗り越えて戻ってきました。
Zボソン

1
@ ulyssis2ナイーブ行列の乗算は間違いなくO(n ^ 3)であり、私が知る限り、計算カーネルはナイーブアルゴリズムを実装します(これは、Strassenが最終的にはるかに多くの操作(加算)を実行するためです。あなたは速い製品を作ることができます、しかし私は間違っているかもしれません)。行列の乗算がO(n ^ 2)であるかどうかは、未解決の問題です。
エタール-コホモロジー2017

通常は、線形代数ライブラリを使用して作業を行うことをお勧めします。Intel MKL、OpenBLASなどの最新のライブラリは、ハードウェアで利用可能な最適な実装を選択する動的CPUディスパッチを提供します(たとえば、SSEよりも広いベクトルレジスタが利用できる場合があります:AVX AVX2、AVX512 ...)。高速なプログラムを取得するために、移植性のないプログラムを作成する必要はありません。
ホルヘベロン

39

これはアプリケーションによって異なりますが、一般に、行列を転置する最も速い方法は、ルックアップを行うときに座標を反転することです。その後、実際にデータを移動する必要はありません。


32
これは、小さなマトリックスの場合、または1回だけ読み取る場合に最適です。ただし、転置行列が大きく、何度も再利用する必要がある場合でも、高速転置バージョンを保存して、メモリアクセスパターンを改善することができます。(+1、ところで)
Agentlien 2013年

2
@Agentlien:なぜA [j] [i]はA [i] [j]よりも遅いのですか?
ビーカー2013年

32
@beaker大きなマトリックスがある場合、異なる行/列が異なるキャッシュ行/ページを占める可能性があります。この場合、隣接する要素に次々にアクセスするように要素を反復処理する必要があります。そうしないと、すべての要素アクセスがキャッシュミスになり、パフォーマンスが完全に低下する可能性があります。
エージェントリエン2013年

10
@beaker:CPUレベルでのキャッシュと関係があり(マトリックスが単一の大きなメモリブロブであると仮定)、キャッシュラインはマトリックスの有効なラインであり、プリフェッチャーは次の数行をフェッチする可能性があります。アクセスを切り替えると、CPUキャッシュ/プリフェッチャーは行ごとに機能しますが、列ごとにアクセスすると、パフォーマンスが大幅に低下する可能性があります。
Matthieu M.

2
@taocp基本的に、転置されたことを示すために何らかのフラグが必要になります。その後、たとえば、リクエスト(i,j)は次のようにマッピングされます(j,i)
Shafik Yaghmour 2013年

5

x86ハードウェアを使用した4x4スクエアフロート(32ビット整数については後で説明します)行列の転置に関する詳細。8x8や16x16などのより大きな正方行列を転置するには、ここから始めると便利です。

_MM_TRANSPOSE4_PS(r0, r1, r2, r3)コンパイラによって実装方法が異なります。GCCとICC(私はClangをチェックしていません)は使用しますがunpcklps, unpckhps, unpcklpd, unpckhpd、MSVCはのみを使用しshufpsます。このように、実際にはこれら2つのアプローチを組み合わせることができます。

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);

興味深い観察の1つは、このように2つのシャッフルを1つのシャッフルと2つのブレンド(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つ多くの命令を使用します。利点は、ポートの圧力を下げることです。これは、状況によってはメリットがある場合があります。現在、すべてのシャッフルとアンパックは1つの特定のポートにのみ移動できますが、ブレンドは2つの異なるポートのいずれかに移動できます。

MSVCのような8つのシャッフルを使用して、それを4つのシャッフル+ 8つのブレンドに変換しようとしましたが、機能しませんでした。私はまだ4つの開梱を使わなければなりませんでした。

私はこれと同じテクニックを8x8フロートトランスポーズに使用しました(その回答の終わり近くを参照)。 https://stackoverflow.com/a/25627536/2542702。その答えでは、私はまだ8つのアンパックを使用する必要がありましたが、8つのシャッフルを4つのシャッフルと8つのブレンドに変換するように管理しました。

32ビット整数の場合shufps(AVX512を使用した128ビットシャッフルを除く)のようなものはないため、ブレンドに(効率的に)変換できるとは思わないアンパックでのみ実装できます。AVX512を使用vshufi32x4するとshufps、32ビットのフロートではなく4つの整数の128ビットレーンを除いて効果的に機能するため、場合によってはこの同じ手法が使用される可能性がありvshufi32x4ます。Knights Landingの場合、シャッフルはブレンドよりも4倍遅くなります(スループット)。


1
shufps整数データで使用できます。多くのシャッフルを実行している場合、特に同等の効率のAVX2が利用できない場合は、すべてをFPドメインでshufps+blendpsで実行する価値があるかもしれませvpblenddん。また、Intel SnBファミリのハードウェアでは、のshufpsような整数命令間で使用するための追加のバイパス遅延はありませんpaddd。(ただし、Agner FogのSnBテストによるblendpspaddd、との混合にはバイパス遅延があります。)
Peter Cordes

@PeterCordes、ドメインの変更をもう一度確認する必要があります。Core2-Skylakeのドメイン変更ペナルティを要約した表(おそらくSOに関する回答)はありますか?いずれにせよ、私はこれについてもっと考えました。なぜ泳ぐのかがわかりました。あなたvinsertf64x4は私の16x16転置の答えでvinserti64x4。の代わりに言及し続けました。行列を読み取ってから書き込む場合、転置はデータを移動するだけなので、浮動小数点ドメインと整数ドメインのどちらを使用するかは問題ではありません。
zボソン2016

1
Agnerの表には、Core2とNehalem(およびAMDだと思います)の命令ごとのドメインがリストされていますが、SnBファミリーはリストされていません。アグナーのマイクロアーチガイドには、いくつかの例を挙げて、SnBでは1cになり、多くの場合0になるという段落があります。Intelの最適化マニュアルには私が思う表がありますが、私はそれを理解しようとはしなかったので、それがどれほど詳細になっているのか覚えていません。私はそれが与えられた命令がでどうなるかカテゴリ全く明らかでない思い出すん。
ピーター・コルド

メモリに書き戻すだけではない場合でも、トランスポーズ全体で1クロックしか追加されません。転置のコンシューマーがシャッフルまたはブレンドによって書き込まれたレジスターの読み取りを開始すると、各オペランドの追加の遅延が並行して(またはずらして)発生する可能性があります。アウトオブオーダー実行では、最後の数回のシャッフルが終了している間に最初の数回のFMAなどを開始できますが、ダイパス遅延の連鎖はなく、最大で1つだけ余分になります。
Peter Cordes 2016

1
Nicwの答え!Intel 64-ia-32-architectures-optimization-manualの表2-3に、Skylakeのバイパス遅延がリストされています。これはおそらく興味深いことです。Haswellの表2-8はかなり異なって見えます。
2016

1

各行を列と見なし、各列を行と見なします.. i、jの代わりにj、iを使用します

デモ: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 ++を初めて使用しますが、これは天才に見えます。ユニオンはメンバーに共有メモリの場所を使用するため、そのメモリを別の方法で読み取ることができます。したがって、新しい配列割り当てを行わなくても、転置行列を取得できます。私は正しいですか?
Doğuş

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
読み取りよりも書き込み時のキャッシュミスペナルティが小さいため、2つのループを交換した方が速いと思います。
phoeagon 2013年

5
これは正方行列に対してのみ機能します。長方形の行列はまったく別の問題です!
nealB 2013年

2
質問は最速の方法を求めています。これはただの方法です。最速は言うまでもなく、何が速いと思いますか?大きなマトリックスの場合、これはキャッシュを破壊し、ひどいパフォーマンスをもたらします。
Eric Postpischil 2013年

1
@NealB:どうやってそれを理解しますか?
Eric Postpischil 2013年

@EricPostpischil OPは比較的大きなマトリックスについて質問しているので、2倍のメモリが割り当てられないように、「インプレース」で実行したいと考えています。これが行われると、送信元と宛先のマトリックスのベースアドレスは同じになります。行と列のインデックスを反転して転置することは、正方行列に対してのみ機能します。長方形の行列に対してこれを正しく行う方法はいくつかありますが、それらはやや複雑です。
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。これは1回のパスで実行できるため、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;
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.