ループを展開するという概念は理解していますが、単純なループを展開する方法を誰かに説明してもらえますか?
ループを見せてから、何が起こっているのかを説明したそのループの展開バージョンを見せていただければ幸いです.
ループを展開するという概念は理解していますが、単純なループを展開する方法を誰かに説明してもらえますか?
ループを見せてから、何が起こっているのかを説明したそのループの展開バージョンを見せていただければ幸いです.
ループのアンローリングが最も効果的な場合、つまり依存関係の連鎖を明確にすることが重要だと思います。依存関係チェーンは、各計算が前の計算に依存する一連の操作です。たとえば、次のループには依存チェーンがあります。
for(i=0; i<n; i++) sum += a[i];
最新のプロセッサのほとんどは、サイクルごとに複数の順不同の操作を実行できます。これにより、命令のスループットが向上します。ただし、順不同の操作は、依存関係チェーンでこれを行うことはできません。上記のループでは、各計算は加算演算のレイテンシによって制限されます。
上記のループでは、このように 2 つの依存関係チェーンに展開できます。
sum1 = 0, sum2 = 0;
for(i=0; i<n/2; i++) sum1 += a[2*i], sum2 += a[2*i+1];
for(i=(n/2)*2; i<n; i++) sum += a[i]; // clean up for n odd
sum += sum1 + sum2;
現在、アウトオブオーダー プロセッサは、いずれかのチェーンで独立して動作し、同時にプロセッサに依存する可能性があります。
一般に、操作のレイテンシにクロック サイクルごとに実行できる操作の数を掛けた値だけアンロールする必要があります。たとえば、x86_64 プロセッサでは、クロック サイクルごとに少なくとも 1 つの SSE 追加を実行でき、SSE 追加のレイテンシは 3 であるため、3 回アンロールする必要があります。Haswell プロセッサを使用すると、クロック サイクルごとに 2 つの FMA 操作を実行でき、各 FMA 操作のレイテンシは 5 であるため、最大スループットを得るには 10 回アンロールする必要があります。
コンパイラに関する限り、GCC は依存チェーンを展開しません ( -funroll-loops
. GCC で展開する必要があります。Clang では 4 回アンロールしますが、これは一般的にかなり良好です (場合によっては、Haswell と Broadwell では 10 回、Skylake では 8 回アンロールする必要があります)。
アンロールするもう 1 つの理由は、ループ内の操作の数が、クロック サイクルごとにプッシュ スルーできる命令の数を超えた場合です。たとえば、次のループで
for(i=0; i<n; i++) b[i] += 3.14159*a[i];
依存チェーンがないため、順不同で実行しても問題ありません。しかし、反復ごとに次の操作を必要とする命令セットを考えてみましょう。
2 SIMD load
1 SIMD store
1 SIMD multiply
1 SIMD addition
1 scalar addition for the loop counter
1 conditional jump
また、プロセッサがサイクルごとにこれらの命令を 5 つプッシュできると仮定しましょう。この場合、反復ごとに 7 つの命令がありますが、サイクルごとに実行できる命令は 5 つだけです。次に、ループ展開を使用して、カウンターへのスカラー加算i
と条件付きジャンプのコストを償却できます。たとえば、ループを完全に展開した場合、これらの命令は必要ありません。
ループカウンターとジャンプのコストを償却するために、-funroll-loops
GCC で正常に動作します。これは 8 回アンロールします。つまり、カウンターの追加とジャンプは、反復ごとではなく、8 回の反復ごとに 1 回実行する必要があります。
巻き(レギュラー):
#define N 44
int main() {
int A[N], B[N];
int i;
// fill A with stuff ...
for(i = 0; i < N; i++) {
B[i] = A[i] * (100 % i);
}
// do stuff with B ...
}
展開:
#define N 44
int main() {
int A[N], B[N];
int i;
// fill A with stuff ...
for(i = 0; i < N; i += 4) {
B[i] = A[i] * (100 % i);
B[i+1] = A[i+1] * (100 % i+1);
B[i+2] = A[i+2] * (100 % i+2);
B[i+3] = A[i+3] * (100 % i+3);
}
// do stuff with B ...
}
展開すると、プログラムのサイズが大きくなりますが、パフォーマンスが向上する可能性があります。パフォーマンスの向上は、分岐ペナルティ、キャッシュ ミス、および実行命令の減少による可能性があります。コード量の増加や読みやすさの低下など、明らかな欠点もあれば、それほど明白ではない欠点もあります。