2048x2048の配列乗算と2047x2047の配列乗算でパフォーマンスに大きな影響があるのはなぜですか?


127

前に「MATLABが行列乗算で非常に高速である理由」で前述したように、いくつかの行列乗算ベンチマークを作成してい ます。

2つの2048x2048行列を乗算すると、C#と他の行列には大きな違いがあります。2047x2047の行列のみを乗算しようとすると、正常に見えます。比較のために他にもいくつか追加しました。

1024x1024-10秒。

1027x1027-10秒。

2047x2047-90秒。

2048x2048-300秒。

2049x2049-91秒。(更新)

2500x2500-166秒

これは、2k x 2kの場合の3.5分差です。

2dim配列の使用

//Array init like this
int rozmer = 2048;
float[,] matice = new float[rozmer, rozmer];

//Main multiply code
for(int j = 0; j < rozmer; j++)
{
   for (int k = 0; k < rozmer; k++)
   {
     float temp = 0;
     for (int m = 0; m < rozmer; m++)
     {
       temp = temp + matice1[j,m] * matice2[m,k];
     }
     matice3[j, k] = temp;
   }
 }

23
これは、上級レベルCプログラミングまたはOSデザインクラスにとってすばらしい試験問題です;-)
Dana the Sane

多次元の[、]配列とギザギザの[] []配列の両方、および32ビットと64ビットの両方をテストしてみましたか?私は数回しかテストしませんでしたが、ギザギザの方が結果と一致しているように見えましたが、ギザギザの64ビットが高かったので、この状況に当てはまるヒューリスティックがjitにあるかどうか、または以前に提案されたようにそのキャッシュに関連しているかどうかはわかりません。GPGPUソリューションが必要な場合は、research.microsoft.com / en-us / projects / acceleratorを使用してください。これは、他の投稿の時代と競合できるはずです。
クリス

やや素朴な質問ですが、2つの正方行列の乗算に必要な演算(加算/乗算)はいくつですか?
ニックT

回答:


61

これはおそらくL2キャッシュの競合に関係しています。

matice1でのキャッシュミスは順次アクセスされるため、問題にはなりません。ただし、matice2の場合、列全体がL2に収まる場合(つまり、matice2 [0、0]、matice2 [1、0]、matice2 [2、0] ...などにアクセスした場合)、何も問題はありません。 matice2のいずれかでキャッシュミス。

変数のバイトアドレスがXの場合、キャッシュラインの(X >> 6)&(L-1)よりも、キャッシュのしくみを詳しく説明します。ここで、Lはキャッシュ内のキャッシュラインの総数です。Lは常に2の累乗です。6は2 ^ 6 == 64バイトがキャッシュラインの標準サイズであることから来ています。

これはどういう意味ですか?それは、アドレスXとアドレスYがあり、(X >> 6)-(Y >> 6)がLで割り切れる場合(つまり、2のべき乗)、同じキャッシュラインに格納されることを意味します。

2048と2049の違いは何ですか?

2048があなたのサイズであるとき:

&matice2 [x、k]と&matice2 [y、k]を取る場合、差(&matice2 [x、k] >> 6)-(&matice2 [y、k] >> 6)は2048 * 4(sizeフロートの)。つまり、2の大きな指数です。

したがって、L2のサイズによっては、キャッシュラインの競合が多数発生し、L2のごく一部のみを使用して列を格納するため、実際には完全な列をキャッシュに格納できず、パフォーマンスが低下します。 。

サイズが2049の場合、差は2049 * 4であり、2の累乗ではないため、競合が少なくなり、列が安全にキャッシュに収まります。

この理論をテストするために、あなたができることがいくつかあります:

このmatice2 [razmor、4096]のように配列matice2配列を割り当て、razmor = 1024、1025または任意のサイズで実行すると、以前のパフォーマンスと比較して非常に悪いパフォーマンスが表示されます。これは、すべての列を強制的に配置して、互いに競合するためです。

次に、matice2 [razmor、4097]を試して、任意のサイズで実行すると、パフォーマンスが大幅に向上するはずです。


最後の2つの段落を間違えましたか?どちらの試みもまったく同じです。:)
Xeo

キャッシュの関連性も役割を果たします。
ベンジャクソン

20

おそらくキャッシング効果。2のべき乗である行列の次元と、2のべき乗でもあるキャッシュサイズを使用すると、L1キャッシュのごく一部しか使用できなくなり、処理速度が大幅に低下します。単純な行列の乗算は、通常、データをキャッシュにフェッチする必要があるため制約されます。タイリングを使用して最適化されたアルゴリズム(またはキャッシュを気にしないアルゴリズム)は、L1キャッシュをより効果的に使用することに重点を置いています。

他のペア(2 ^ n-1,2 ^ n)の時間を計ると、同様の効果が得られると思います。

より完全に説明すると、matice2 [m、k]にアクセスする内側のループでは、matice2 [m、k]とmatice2 [m + 1、k]が2048 * sizeof(float)だけ互いにオフセットされている可能性がありますしたがって、L1キャッシュ内の同じインデックスにマップされます。Nウェイ連想キャッシュでは、通常、これらすべてのキャッシュの場所が1〜8個あります。したがって、これらのアクセスのほとんどすべてが、L1キャッシュの追い出しと、より遅いキャッシュまたはメインメモリからのデータのフェッチをトリガーします。


+1。おそらく聞こえます。キャッシュの結合性に注意する必要があります。
マッケ'19年

16

これは、CPUキャッシュのサイズに関係している可能性があります。マトリックスマトリックスの2行が収まらない場合は、RAMからの要素のスワッピングに時間がかかります。追加の4095要素は、行がフィットしないようにするのに十分な場合があります。

あなたのケースでは、2047 2d行列の2行が16KBのメモリ内に収まります(32ビットタイプを想定)。たとえば、64KBのL1キャッシュ(バス上のCPUに最も近い)がある場合、少なくとも4行(2047 * 32)を一度にキャッシュに収めることができます。より長い行では、行のペアを16KBを超えてプッシュするパディングが必要な場合、状況が乱雑になり始めます。また、キャッシュを「ミス」するたびに、別のキャッシュまたはメインメモリからデータをスワップインすると、遅延が発生します。

私の推測では、さまざまなサイズの行列で見られる実行時間の違いは、オペレーティングシステムが利用可能なキャッシュをどの程度効果的に利用できるかに影響されます(一部の組み合わせは問題があるだけです)。もちろん、これはすべて私の側での単純化です。


2
しかし、彼が16.7 MBのCPUキャッシュを持っていることはほとんどありません
MarinoŠimićMay

結果を2049x2049-91秒で更新しました。それが「キャッシュの問題」だった場合、これはまだ300秒以上ではないでしょうか?
ウルフ

@Marino回答が更新され、それを考慮に入れました。
Dana the Sane

1
これらの説明はどれも、問題を引き出すさまざまなまばらなサイズに関する新しい詳細に適切に対処できず、その間に他の人が影響を受けていないように感じます。
Ken Rockot、

2
この説明は正しくないと思います。問題は、サイズが2の累乗である場合、キャッシュラインの競合が原因でキャッシュ容量を完全に利用していないことです。また、オペレーティングシステムはキャッシュとは何の関係もありません。キャッシュするものと削除するものを決定するのはOSではないためです。ハードウェアで。OSはデータの配置と関係がありますが、この場合、C#がデータを割り当てる方法とメモリ内の2D配列を表す方法がすべてであり、OSはそれとは関係ありません。
zviadm


5

より大きなサイズで時間が減少していることを考えると、特に問題のあるマトリックスサイズに対して2の累乗を使用すると、キャッシュの競合が発生する可能性が高くなりますか?私はキャッシュの問題の専門家ではありませんが、キャッシュに関連するパフォーマンスの問題に関する優れた情報はここにあります


キャッシュ連想性に関するリンクのセクション5が特に当てはまるようです。
Dana the Sane

4

matice2垂直方向に配列にアクセスしていると、配列はキャッシュ内とキャッシュ外でより多くスワップされます。配列を斜めにミラーリングして、の[k,m]代わりに配列にアクセスできるようにする[m,k]と、コードの実行速度が大幅に向上します。

私はこれを1024x1024マトリックスでテストしましたが、約2倍の速さです。2048x2048マトリックスでは、約10倍高速です。


2049年にはより速い2048である理由は説明していません
マッケ

@Macke:これは、メモリキャッシングの制限を通過するため、キャッシュミスが大幅に増えるためです。
グッファ

なぜ反対票か。あなたが間違っていると思うことを言わなければ、それは答えを改善することはできません。
Guffa

説明のないもう1つの反対票...私の回答の中で「おそらく」、「推測」、「すべき」の数が少なすぎるのでしょうか。
Guffa

4

キャッシュのエイリアシング

または、用語を作成できる場合は、キャッシュのスラッシング

キャッシュは、低次ビットでのインデックス付けと高次ビットでのタグ付けによって機能します。

キャッシュが4ワードで、マトリックスが4 x 4であるというイメージング。列がアクセスされ、行が2の累乗である場合、メモリ内の各列要素は同じキャッシュ要素にマップされます。

2の1の累乗は、実際にはこの問題に最適です。新しい各列要素は、行でアクセスする場合とまったく同じように、次のキャッシュスロットにマップされます。

実際には、タグは連続して増加する複数のアドレスをカバーし、隣接するいくつかの要素を続けてキャッシュします。新しい各行がマップするバケットをオフセットすることにより、列をトラバースしても以前のエントリは置き換えられません。次の列がトラバースされると、キャッシュ全体がさまざまな行で埋められ、キャッシュに収まる各行セクションがいくつかの列にヒットします。

キャッシュはDRAMよりもはるかに高速であるため(主にオンチップであるため)、ヒット率がすべてです。


2

キャッシュサイズの制限に達したか、タイミングの再現性に問題がある可能性があります。

問題が何であれ、自分で行列乗算をC#で記述せず、代わりにBLASの最適化バージョンを使用する必要があります。そのサイズの行列は、最新のマシンでは1秒未満で乗算する必要があります。


1
私はBLASを知っていますが、できるだけ速くすることではなく、さまざまな言語で記述してテストすることが課題でした。これは私にとって非常に奇妙な問題であり、Iamは結果がどうなっているのかを本当に知りたいと思っています。
ウルフ

3
@Wolf 1秒かかるはずのものが90秒と300秒のどちらにかかっているのかわくわくするのは難しいと思います。
David Heffernan、

4
何かがどのように機能するかを学ぶ最良の方法は、それを自分で記述し、実装をどのように改善できるかを確認することです。これが(うまくいけば)ウルフがやっていることです。
Callum Rogers、

@Callum Rogers、同意した。このようにして、ファイルコピー操作におけるバッファサイズの重要性を学びました。
ケリーS.フランス語

1

キャッシュ階層を効果的に利用することは非常に重要です。多次元配列のデータが適切に配置されていることを確認する必要があります。これは、をタイリングすることで実現できます。これを行うには、2D配列を1D配列として、インデックスメカニズムとともに格納する必要があります。従来の方法の問題は、同じ行にある2つの隣接する配列要素がメモリ内で互いに隣接しているにもかかわらず、同じ列内の2つの隣接する要素がメモリ内のW要素によって分離されることです(Wは列の数) 。タイリングにより、パフォーマンスが10倍にもなる場合があります。


Hmm-2Dとして宣言された配列(float [、] matice = new float [rozmer、rozmer];)は、RAMに1次元配列としてのみ割り当てられ、内部で行/ストライド計算が行われます。それでは、なぜ1Dとして宣言して手動で行/ストライド計算を行う方が速いのでしょうか?sol'nは、大きな配列がキャッシュに収まる小さなタイルの配列として大きな配列を割り当てることを意味しますか?
エリックM

1
ライブラリまたは使用しているツールがタイリングを行う場合、その必要はありません。ただし、たとえばC / C ++で従来の2D配列を使用する場合は、タイリングによってパフォーマンスが向上します。
Arlen

0

シーケンシャルフラッディング」と呼ばれるものの結果だと思います。これは、キャッシュサイズよりわずかに大きいオブジェクトのリストをループしようとしているため、リスト(配列)へのすべてのリクエストはRAMから実行する必要があり、1つのキャッシュを取得しないということです。ヒット。

あなたの場合、あなたはあなたの配列を2048回インデックスで2048回ループしていますが、あなたは2047のためのスペースしか持っていないので(おそらく配列構造からのオーバーヘッドのため)、配列posにアクセスするたびに、この配列posを取得する必要がありますラムから。その後、キャッシュに格納されますが、再び使用される直前にダンプされます。したがって、キャッシュは本質的に役に立たず、実行時間がはるかに長くなります。


1
不正解です。2049は2048よりも速く、これはあなたの主張に反駁します。
マッケ'19年

@マッケ:それはかなり可能です。しかし、そこにあるわずかな彼のプロセッサで使用されるキャッシュポリシーは、まだこのdescisionを作るかもしれないというチャンスが。それはあまりありそうにありませんが、考えられないことではありません。
Automatico、
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.