GCC (私は 4.8.4 を使用しています) にwhile
一番下の関数のループを完全に展開するように指示する方法はありますか?つまり、このループをピールしますか? ループの反復回数は、コンパイル時にわかっています: 58。
最初に私が試したことを説明しましょう。
GAS出力を確認することにより:
gcc -fpic -O2 -S GEPDOT.c
XMM0 ~ XMM11 の 12 本のレジスタを使用します。フラグ-funroll-loops
を gcc に渡すと、次のようになります。
gcc -fpic -O2 -funroll-loops -S GEPDOT.c
ループは 2 回だけ展開されます。GCC 最適化オプションを確認しました。GCC はそれ-funroll-loops
もオンにすると言い-frename-registers
ます。そのため、GCC がループをアンロールするとき、レジスタ割り当てのための以前の選択は、「残りの」レジスタを使用することです。しかし、XMM12 から XMM15 までは 4 つしか残っていないため、GCC は最高でも 2 回しか展開できません。利用可能な XMM レジスタが 16 個ではなく 48 個ある場合、GCC は問題なく while ループを 4 回アンロールします。
それでも私は別の実験をしました。最初に while ループを手動で 2 回展開し、 function を取得しましたGEPDOT_2
。そしたら全然違いがない
gcc -fpic -O2 -S GEPDOT_2.c
と
gcc -fpic -O2 -funroll-loops -S GEPDOT_2.c
GEPDOT_2
すでにすべてのレジスタを使用しているため、展開は実行されません。
GCC は、誤った依存関係が導入される可能性を回避するために、レジスタの名前変更を行います。しかし、私の中にはそのような可能性がないことは確かGEPDOT
です。あったとしても、それは重要ではありません。ループを自分で展開してみましたが、4 回展開すると 2 回展開するよりも速く、展開しないよりも高速です。もちろん、手動で何度も展開できますが、面倒です。GCCは私のためにこれを行うことができますか? ありがとう。
// C file "GEPDOT.c"
#include <emmintrin.h>
void GEPDOT (double *A, double *B, double *C) {
__m128d A1_vec = _mm_load_pd(A); A += 2;
__m128d B_vec = _mm_load1_pd(B); B++;
__m128d C1_vec = A1_vec * B_vec;
__m128d A2_vec = _mm_load_pd(A); A += 2;
__m128d C2_vec = A2_vec * B_vec;
B_vec = _mm_load1_pd(B); B++;
__m128d C3_vec = A1_vec * B_vec;
__m128d C4_vec = A2_vec * B_vec;
B_vec = _mm_load1_pd(B); B++;
__m128d C5_vec = A1_vec * B_vec;
__m128d C6_vec = A2_vec * B_vec;
B_vec = _mm_load1_pd(B); B++;
__m128d C7_vec = A1_vec * B_vec;
A1_vec = _mm_load_pd(A); A += 2;
__m128d C8_vec = A2_vec * B_vec;
B_vec = _mm_load1_pd(B); B++;
int k = 58;
/* can compiler unroll the loop completely (i.e., peel this loop)? */
while (k--) {
C1_vec += A1_vec * B_vec;
A2_vec = _mm_load_pd(A); A += 2;
C2_vec += A2_vec * B_vec;
B_vec = _mm_load1_pd(B); B++;
C3_vec += A1_vec * B_vec;
C4_vec += A2_vec * B_vec;
B_vec = _mm_load1_pd(B); B++;
C5_vec += A1_vec * B_vec;
C6_vec += A2_vec * B_vec;
B_vec = _mm_load1_pd(B); B++;
C7_vec += A1_vec * B_vec;
A1_vec = _mm_load_pd(A); A += 2;
C8_vec += A2_vec * B_vec;
B_vec = _mm_load1_pd(B); B++;
}
C1_vec += A1_vec * B_vec;
A2_vec = _mm_load_pd(A);
C2_vec += A2_vec * B_vec;
B_vec = _mm_load1_pd(B); B++;
C3_vec += A1_vec * B_vec;
C4_vec += A2_vec * B_vec;
B_vec = _mm_load1_pd(B); B++;
C5_vec += A1_vec * B_vec;
C6_vec += A2_vec * B_vec;
B_vec = _mm_load1_pd(B);
C7_vec += A1_vec * B_vec;
C8_vec += A2_vec * B_vec;
/* [write-back] */
A1_vec = _mm_load_pd(C); C1_vec = A1_vec - C1_vec;
A2_vec = _mm_load_pd(C + 2); C2_vec = A2_vec - C2_vec;
A1_vec = _mm_load_pd(C + 4); C3_vec = A1_vec - C3_vec;
A2_vec = _mm_load_pd(C + 6); C4_vec = A2_vec - C4_vec;
A1_vec = _mm_load_pd(C + 8); C5_vec = A1_vec - C5_vec;
A2_vec = _mm_load_pd(C + 10); C6_vec = A2_vec - C6_vec;
A1_vec = _mm_load_pd(C + 12); C7_vec = A1_vec - C7_vec;
A2_vec = _mm_load_pd(C + 14); C8_vec = A2_vec - C8_vec;
_mm_store_pd(C,C1_vec); _mm_store_pd(C + 2,C2_vec);
_mm_store_pd(C + 4,C3_vec); _mm_store_pd(C + 6,C4_vec);
_mm_store_pd(C + 8,C5_vec); _mm_store_pd(C + 10,C6_vec);
_mm_store_pd(C + 12,C7_vec); _mm_store_pd(C + 14,C8_vec);
}
更新 1
@ user3386109 のコメントのおかげで、この質問を少し広げたいと思います。@ user3386109 は非常に良い質問をします。実際、スケジュールする並列命令が非常に多い場合に、最適なレジスタ割り当てを行うコンパイラの能力に疑問があります。
個人的には、最初にasmインライン アセンブリでループ本体 (HPC の鍵となる) をコーディングし、それを何度でも複製するのが確実な方法だと思います。今年の初めに人気のない投稿がありました: inline assembly . ループの反復回数 j は関数の引数であるため、コンパイル時には不明であるため、コードは少し異なります。その場合、ループを完全に展開することはできないので、アセンブリ コードを 2 回だけ複製し、ループをラベルに変換してジャンプしました。作成したアセンブリの結果として得られるパフォーマンスは、コンパイラが生成したアセンブリよりも約 5% 高いことがわかりました。これは、コンパイラが予想される最適な方法でレジスタを割り当てることができないことを示唆している可能性があります。
私は (そして今も) アセンブリ コーディングの初心者だったので、x86 アセンブリについて少し学ぶための良いケース スタディになりました。GEPDOT
しかし、長い目で見れば、私はアセンブリの割合が大きいコードを書く傾向はありません。主に次の3つの理由があります。
- asmインライン アセンブリは、移植性がないと批判されています。理由はわかりませんが。おそらく、マシンごとに異なるレジスタが上書きされているためでしょうか?
- コンパイラも良くなっています。したがって、コンパイラーが適切な出力を生成するのを支援するために、アルゴリズムの最適化と C コーディングの習慣を改善することを引き続き好みます。
- 最後の理由はもっと重要です。反復回数は必ずしも 58 回とは限りません。高性能の行列分解サブルーチンを開発しています。キャッシュ ブロック ファクターの
nb
場合、反復回数は になりますnb-2
。nb
以前の投稿で行ったように、関数の引数として指定するつもりはありません。これはマシン固有のパラメータで、マクロとして定義されます。したがって、反復回数はコンパイル時にわかりますが、マシンごとに異なる場合があります。さまざまなnb
. したがって、ループを剥がすようにコンパイラに指示するだけの方法があれば、それは素晴らしいことです。
高性能でありながらポータブルなライブラリを作成する経験も共有していただければ幸いです。