ループの展開により、パフォーマンスが非常に重要なコード(モンテカルロシミュレーション内では何百万回と呼ばれるクイックソートアルゴリズム)を最適化しようとしています。ここに私がスピードアップしようとしている内側のループがあります:
// Search for elements to swap.
while(myArray[++index1] < pivot) {}
while(pivot < myArray[--index2]) {}
私は次のようなものに展開しようとしました:
while(true) {
if(myArray[++index1] < pivot) break;
if(myArray[++index1] < pivot) break;
// More unrolling
}
while(true) {
if(pivot < myArray[--index2]) break;
if(pivot < myArray[--index2]) break;
// More unrolling
}
これはまったく違いがなかったので、読みやすい形式に戻しました。ループの展開を試みたときも、同じような経験をしました。最新のハードウェア上の分岐予測子の品質を考えると、ループ展開がいつでも有用な最適化であるとしたらどうでしょうか?
依存関係チェーンを解除できる場合、ループの展開は理にかなっています。これにより、CPUの順序が乱れたり、スーパースカラーが発生したりすることで、スケジュールをより適切に実行できるため、実行速度が向上します。
簡単な例:
for (int i=0; i<n; i++)
{
sum += data[i];
}
ここで、引数の依存関係チェーンは非常に短いです。データ配列にキャッシュミスがあるためにストールした場合、CPUは待機する以外何もできません。
一方、このコード:
for (int i=0; i<n; i+=4)
{
sum1 += data[i+0];
sum2 += data[i+1];
sum3 += data[i+2];
sum4 += data[i+3];
}
sum = sum1 + sum2 + sum3 + sum4;
より高速に実行できます。 1つの計算でキャッシュミスまたは他のストールが発生した場合、ストールに依存しない3つの依存関係チェーンがまだあります。異常なCPUがこれらを実行できます。
同じ数の比較を行っているので、それらは違いを生みません。これがより良い例です。の代わりに:
for (int i=0; i<200; i++) {
doStuff();
}
書きます:
for (int i=0; i<50; i++) {
doStuff();
doStuff();
doStuff();
doStuff();
}
その場合でも、ほぼ間違いなく重要ではありませんが、現在では200回ではなく50回の比較を実行しています(比較はより複雑であると考えてください)。
手動ループの展開は一般に、主に歴史の産物です。それは、優れたコンパイラが重要なときにあなたのために行うであろうものの成長しているリストのもう一つです。たとえば、ほとんどの人はわざわざx <<= 1
またはx += x
の代わりに x *= 2
。あなたはただx *= 2
とコンパイラーは最適なものに最適化します。
基本的に、コンパイラを推測する必要性はますます少なくなります。
最新のハードウェアでの分岐予測に関係なく、ほとんどのコンパイラはとにかくループ展開を行います。
コンパイラーがどの程度最適化を行っているかを知ることは価値があります。
私は Felix von Leitnerのプレゼンテーション をテーマについて非常に啓発的に見つけました。読むことをお勧めします。概要:最新のコンパイラーは非常に賢いので、手の最適化はほとんど効果的ではありません。
ループのアンロールは、手によるアンロールでもコンパイラのアンロールでも、特に最近のx86 CPU(Core 2、Core i7)では生産性が低下することがよくあります。ボトムライン:このコードを展開する予定のCPUでループを展開する場合としない場合のコードのベンチマーク。
私が理解している限り、現代のコンパイラはすでに適切な場所でループを展開しています-たとえば、gcc、最適化フラグが渡された場合、マニュアルには次のように記載されています:
コンパイル時またはループに入るときに反復回数を決定できるループを展開します。
したがって、実際には、コンパイラが自明なケースを実行する可能性があります。したがって、コンパイラーが必要な反復回数をコンパイラーが簡単に判断できるようにするのは、できる限り多くのループを作成することです。
知らずに試すことは、それを行う方法ではありません。
この並べ替えには全体の時間の割合が高くなりますか?
すべてのループの展開は、インクリメント/デクリメント、停止条件の比較、ジャンプのループオーバーヘッドを削減します。ループで実行している処理が、ループのオーバーヘッド自体よりも多くの命令サイクルを必要とする場合、パーセンテージで大幅な改善は見られません。
ループの展開は、特定の場合に役立ちます。唯一の利点は、いくつかのテストをスキップしないことです!
たとえば、スカラー置換、ソフトウェアプリフェッチの効率的な挿入が可能です。積極的に展開することにより、実際にどれだけ便利であるか(-O3を使用してもほとんどのループで10%の速度を簡単に上げることができます)に驚くでしょう。
前にも言ったように、それはループに大きく依存し、コンパイラと実験が必要です。ルールを作成するのは困難です(または、展開のためのコンパイラヒューリスティックは完璧でしょう)。
ループの展開は、問題のサイズに完全に依存します。これは、アルゴリズムをより小さな作業グループにサイズを縮小できるかどうかに完全に依存しています。上記で行ったことは、そのようには見えません。モンテカルロシミュレーションを展開できるかどうかはわかりません。
ループの展開の良いシナリオは、画像を回転させることです。別々の作業グループをローテーションできるので。これを機能させるには、反復回数を減らす必要があります。
ループ内およびループ内の両方に多くのローカル変数がある場合、ループの展開は依然として有用です。ループインデックス用にレジスタを保存するのではなく、これらのレジスタを再利用します。
この例では、レジスタを使いすぎないように、少量のローカル変数を使用しています。
比較が(ループエンドへの)比較も重い場合(つまり、_test
以外の命令)、特に外部関数に依存している場合、大きな欠点です。
ループの展開は、分岐予測に対するCPUの認識を高めるのにも役立ちますが、いずれにしても発生します。