web-dev-qa-db-ja.com

Cでの最適化された行列乗算

行列乗算のさまざまな方法を比較しようとしています。最初の方法は通常の方法です。

do
{
    for (j = 0; j < i; j++)
    {
        for (k = 0; k < i; k++)
        {
            suma = 0;
            for (l = 0; l < i; l++)
                suma += MatrixA[j][l]*MatrixB[l][k];
                MatrixR[j][k] = suma;
            }
        }
    }
    c++;
} while (c<iteraciones);

2つ目は、行列Bを最初に転置してから、行による乗算を実行することで構成されます。

int f, co;
for (f = 0; f < i; f++) {
    for ( co = 0; co < i; co++) {
        MatrixB[f][co] = MatrixB[co][f];
    }
}

c = 0;
do
{
    for (j = 0; j < i; j++)
    {
        for (k = 0; k < i; k++)
        {
            suma = 0;
            for (l = 0; l < i; l++)
                suma += MatrixA[j][l]*MatrixB[k][l];
                MatrixR[j][k] = suma;
            }
        }
     }
     c++;
} while (c<iteraciones);

2番目の方法は、連続したメモリスロットにアクセスしているため、はるかに高速であるはずですが、パフォーマンスは大幅に向上していません。私は何か間違ったことをしていますか?

完全なコードを投稿できますが、必要ではないと思います。

22
Peter

すべてのプログラマがメモリについて知っておくべきこと (pdfリンク)by Ulrich Drepperは、メモリ効率について多くの優れたアイデアを持っていますが、特に、メモリについての知識とその使用方法の例として、行列乗算を使用しています。知識はこのプロセスをスピードアップできます。彼の論文の付録A.1を見て、セクション6.2.1を読んでください。論文の表6.2は、1000x1000マトリックスの単純な実装時間から実行時間を10%にできることを示しています。

確かに、彼の最終的なコードはかなり複雑で、システム固有のものやコンパイル時のチューニングを多く使用していますが、それでも本当に速度が必要な場合は、その論文を読み、彼の実装を読むことは間違いなく価値があります。

24
Alok Singhal

これを正しく行うことは簡単なことではありません。大きな行列で特に重要な最適化の1つは、乗算をタイリングして、キャッシュにデータを保持することです。私はかつて12倍のパフォーマンスの違いを測定しましたが、具体的にはキャッシュの倍数を消費するマトリックスサイズを選択しました('97年頃なのでキャッシュは小さかった)。

この主題に関する文献はlotあります。開始点は次のとおりです。

http://en.wikipedia.org/wiki/Loop_tiling

より詳細な調査については、次の参考文献、特にバナージーの書籍が役立つ場合があります。

[Ban93] Banerjee、Utpal、Loop Transformations for Restructuring Compilers:The Foundations、Kluwer Academic Publishers、マサチューセッツ州ノーウェル、1993年。

[Ban94] Banerjee、Utpal、Loop Parallelization、Kluwer Academic Publishers、Norwell、MA、1994。

[BGS93]ベーコン、デビッドF.、スーザンL.グラハム、およびオリバーシャープ、高性能コンピューティング向けコンパイラ変換、カリフォルニア大学バークレー、コンピュータサイエンス部門、テクニカルレポートNo UCB/CSD-93-781。

[LRW91]ラム、モニカS.、エドワードE.ロスバーグ、マイケルEウルフ。 1991年4月、63-74年にカリフォルニア州サンタクララで開催された、プログラミング言語のアーキテクチャサポートに関する第4回国際会議で、ブロックアルゴリズムのキャッシュパフォーマンスと最適化。

[LW91]ラム、モニカS.、マイケルEウルフ。ループ変換理論と並列化を最大化するアルゴリズム、IEEE Transactions on Parallel and Distributed Systems、1991、2(4):452-471。

[PW86]パドヴァ、デビッドA.、およびマイケルJ.ウォルフ、スーパーコンピュータ向けの高度なコンパイラ最適化、ACMの通信、29(12):1184-1201、1986。

[Wolfe89] Wolfe、Michael J.スーパーコンピュータ用のスーパーコンパイラの最適化、MIT Press、ケンブリッジ、MA、1989。

[Wolfe96] Wolfe、Michael J.、並列コンピューティング向け高性能コンパイラ、Addison-Wesley、CA、1996。

13
Peeter Joot

注意:2番目の実装にバグがあります

for (f = 0; f < i; f++) {
    for (co = 0; co < i; co++) {
        MatrixB[f][co] = MatrixB[co][f];
    }
}

F = 0、c = 1を実行すると

        MatrixB[0][1] = MatrixB[1][0];

上書きしますMatrixB[0][1]そしてその値を失います!ループがf = 1、c = 0になると

        MatrixB[1][0] = MatrixB[0][1];

コピーされた値は、すでにそこにあったものと同じです。

7
pmg

マトリックスが十分に大きくない場合、または操作を何度も繰り返さない場合は、かなりの違いは見られません。

たとえば、マトリックスが1,000x1,000の場合は改善が見られますが、100x100未満の場合は心配する必要はありません。

また、yoyが非常に大きな行列で作業しているか、操作を数千回繰り返していない限り、「改善」はミリ秒のオーダーである可能性があります。

最後に、使用しているコンピュータをより高速のものに変更すると、違いはさらに狭くなります。

4
Pablo Rodriguez

行列の乗算は記述しないでください。外部ライブラリに依存する必要があります。特に、GEMMライブラリのBLASルーチンを使用する必要があります。 GEMMは多くの場合、以下の最適化を提供します

ブロッキング

効率的な行列乗算は、行列をブロックし、いくつかの小さなブロックされた乗算を実行することに依存しています。理想的には、各ブロックのサイズは、キャッシュにうまく収まるように選択され、パフォーマンスが大幅に向上します

チューニング

理想的なブロックサイズは、基になるメモリ階層(キャッシュの大きさ)によって異なります。その結果、ライブラリは特定のマシンごとに調整およびコンパイルする必要があります。これは、特にATLASBLAS実装によって行われます。

アセンブリレベルの最適化

行列の乗算は非常に一般的であるため、開発者は手動で最適化します。特に、これはGotoBLASで行われます。

異種(GPU)コンピューティング

Matrix Multiplyは非常にFLOP /計算を集中的に行うため、GPUで実行するのに理想的な候補です。 cuBLASMAGMAはこれに適した候補です。

つまり、密な線形代数はよく研究されたトピックです。人々はこれらのアルゴリズムの改善に人生を捧げています。あなたは彼らの仕事を使うべきです。それは彼らを幸せにするでしょう。

2
MRocklin

それほど特別ではありませんが、より優れています:

    c = 0;
do
{
    for (j = 0; j < i; j++)
    {
        for (k = 0; k < i; k++)
        {
            sum = 0; sum_ = 0;
            for (l = 0; l < i; l++) {
                MatrixB[j][k] = MatrixB[k][j];
                sum += MatrixA[j][l]*MatrixB[k][l];
                l++;
                MatrixB[j][k] = MatrixB[k][j];
                sum_ += MatrixA[j][l]*MatrixB[k][l];

                sum += sum_;
            }
            MatrixR[j][k] = sum;
        }
     }
     c++;
} while (c<iteraciones);
1
user3089939

2つのN * N行列の乗算の計算の複雑さはO(N ^ 3)です。おそらくMATLABで採用されているO(N ^ 2.73)アルゴリズムを使用すると、パフォーマンスが劇的に向上します。 MATLABをインストールした場合は、2つの1024 * 1024行列を乗算してみてください。私のコンピューターでは、MATLABはそれを0.7秒で完了しますが、あなたのような単純なアルゴリズムのC\C++実装には20秒かかります。パフォーマンスに本当に関心がある場合は、複雑度の低いアルゴリズムを参照してください。 O(N ^ 2.4)アルゴリズムがあると聞きましたが、他の操作を無視できるように非常に大きな行列が必要です。

1
user2277473

マトリックスサイズの範囲に対する2つのアプローチを比較したデータを投稿できますか?期待が現実的ではなく、2番目のバージョンの方が速いかもしれませんが、まだ測定を行っていません。

実行時間を測定するときは、matrixBを転置する時間を含めることを忘れないでください。

コードのパフォーマンスをBLASライブラリの同等の操作のパフォーマンスと比較することもできます。これはあなたの質問に直接答えないかもしれませんが、あなたがあなたのコードから何を期待するかもしれないかについてより良い考えをあなたに与えます。

どれほど大きな改善が得られるかは、次の条件によって異なります。

  1. キャッシュのサイズ
  2. キャッシュラインのサイズ
  3. キャッシュの関連性の程度

小さなマトリックスサイズと最新のプロセッサでは、最初に触れた後、MatrixAMatrixBの両方のデータがほぼ完全にキャッシュに保持される可能性が高くなります。

1
Andreas Brinck

あなたが試してみるだけのことです(ただし、これは大きな行列の場合にのみ違いがあります):次のように、内部ループの乗算ロジックから加算ロジックを分離します。

for (k = 0; k < i; k++)
{
    int sums[i];//I know this size declaration is illegal in C. consider 
            //this pseudo-code.
    for (l = 0; l < i; l++)
        sums[l] = MatrixA[j][l]*MatrixB[k][l];

    int suma = 0;
    for(int s = 0; s < i; s++)
       suma += sums[s];
}

これは、sumaに書き込むときにパイプラインが機能しなくなるためです。確かに、これらの多くはレジスタの名前変更などで処理されますが、ハードウェアについての私の限られた理解があれば、コードからすべてのオンスのパフォーマンスを絞り出したければ、これを行うでしょう。パイプラインをストールして、スマへの書き込みを待ちます。乗算は加算よりも費用がかかるため、マシンにできるだけ並列化させたいので、加算のためにストールを保存すると、乗算ループよりも加算ループで待機する時間が少なくなります。

これは私の論理です。地域でより多くの知識を持つ他の人は反対するかもしれません。

1
San Jacinto

非常に古い質問ですが、私のopenglプロジェクトの現在の実装は次のとおりです。

typedef float matN[N][N];

inline void matN_mul(matN dest, matN src1, matN src2)
{
    unsigned int i;
    for(i = 0; i < N^2; i++)
    {
        unsigned int row = (int) i / 4, col = i % 4;
        dest[row][col] = src1[row][0] * src2[0][col] +
                         src1[row][1] * src2[1][col] +
                         ....
                         src[row][N-1] * src3[N-1][col];
    }
}

ここで、Nは行列のサイズに置き換えられます。したがって、4x4行列を乗算する場合は、次を使用します。

typedef float mat4[4][4];    

inline void mat4_mul(mat4 dest, mat4 src1, mat4 src2)
{
    unsigned int i;
    for(i = 0; i < 16; i++)
    {
        unsigned int row = (int) i / 4, col = i % 4;
        dest[row][col] = src1[row][0] * src2[0][col] +
                         src1[row][1] * src2[1][col] +
                         src1[row][2] * src2[2][col] +
                         src1[row][3] * src2[3][col];
    }
}

この関数は主にループを最小化しますが、係数は負担になる可能性があります...私のコンピューターでは、この関数はトリプルforループ乗算関数よりも約50%速く実行されました。

短所:

  • 多くのコードが必要(例:mat3 x mat3、mat5 x mat5 ...のさまざまな関数)

  • 不規則な乗算に必要な微調整(例:mat3 x mat4).....

0
Jas

一般的に言えば、Bの転置すべきは単純な実装よりもはるかに高速になりますが、NxN相当のメモリが無駄に消費されます。私は1週間かけて行列乗算の最適化について調べましたが、これまでのところ絶対的な勝者は次のとおりです。

for (int i = 0; i < N; i++)
    for (int k = 0; k < N; k++)
        for (int j = 0; j < N; j++)
            if (likely(k)) /* #define likely(x) __builtin_expect(!!(x), 1) */
                C[i][j] += A[i][k] * B[k][j];
            else
                C[i][j] = A[i][k] * B[k][j];

これは、基になるCPUのキャッシュプロパティに関係なく最適に機能するため、前のコメントで述べたDrepperの方法よりも優れています。トリックは、3つの行列すべてが行優先順にアクセスされるようにループを並べ替えることにあります。

0
Mike Benden

少ない数で作業している場合、言及している改善は無視できます。また、パフォーマンスは、実行しているハードウェアによって異なります。しかし、数百万の数値に取り組んでいる場合、それは影響します。プログラムに来て、あなたが書いたプログラムを貼り付けることができますか?.

0
Roopesh Majeti