ループの展開がまだ役立つ場合はいつですか?


93

ループのアンロールによって、パフォーマンスが非常に重要なコード(モンテカルロシミュレーション内で何百万回も呼び出されるクイックソートアルゴリズム)を最適化しようとしています。これは私がスピードアップしようとしている内側のループです:

// 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
}

これはまったく違いがなかったので、より読みやすい形式に戻しました。ループのアンロールを試みたときも、同様の経験をしました。最新のハードウェアでの分岐予測子の品質を考えた場合、ループの展開が依然として最適な有用な最適化となるのはいつですか?


1
標準ライブラリのクイックソートルーチンを使用していない理由を教えてください。
Peter Alexander

14
@Poita:私が行っている統計計算に必要ないくつかの追加機能があり、ユースケースに合わせて非常に高度に調整されているため、標準ライブラリよりも一般的ではありませんが、かなり高速です。私は古いプログラミングオプティマイザーを備えたDプログラミング言語を使用しています。ランダムフロートの大規模な配列の場合でも、GCCのC ++ STLソートを10〜20%上回ることができます。
dsimcha 2010

回答:


122

依存関係のチェーンを解除できる場合、ループの展開は理にかなっています。これにより、順不同またはスーパースカラー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がこれらを実行できます。


2
ありがとう。このスタイルでループのアンロールをライブラリ内の他のいくつかの場所で試してみましたが、ここで合計などを計算していますが、これらの場所では驚くほどうまくいきます。その理由は、あなたが示唆しているように、命令レベルの並列処理が増えるためだと思います。
dsimcha 2010

2
良い答えと有益な例。この特定の例では、キャッシュミスによるストールがパフォーマンスどのように影響するかはわかりません。2つ目のコードのパフォーマンスの違い(私のマシンでは、2つ目のコードは2〜3倍高速です)を説明するようになりました。2番目は、スーパースカラーCPUが最大4つの浮動小数点加算を同時に実行できるようにします。
Toby Brull 2014年

2
この方法で合計を計算する場合、結果は元のループと数値的に同一ではないことに注意してください。
バラバス2016年

ループで運ばれる依存関係は、1サイクルの追加です。OoOコアは問題なく動作します。ここでアンロールは浮動小数点SIMDに役立ちますが、それはOoOに関するものではありません。
Veedrac 2017年

2
@ニルス:あまりない; メインストリームのx86 OoO CPUは、まだCore2 / Nehalem / K10に十分似ています。キャッシュミスの後に追いつくことはまだ非常に小さなものでしたが、FPレイテンシを隠すことが依然として大きな利点でした。2010年には、1クロックあたり2ロードを実行できるCPUはさらに珍しいため(SnBがまだリリースされていないため、AMDのみ)、整数コードにとって複数のアキュムレータの価値は現在よりも明らかに低くなっています(もちろん、これは自動ベクトル化するスカラーコードです) 、コンパイラが複数のアキュムレータをベクトル要素に変換するのか、それとも複数のベクトルアキュムレータに変換するのかを知っているのは誰でしょうか。)
Peter Cordes

25

同じ数の比較を行っているので、これらは違いを生じません。これはより良い例です。の代わりに:

for (int i=0; i<200; i++) {
  doStuff();
}

書く:

for (int i=0; i<50; i++) {
  doStuff();
  doStuff();
  doStuff();
  doStuff();
}

それでもほとんど問題にならないでしょうが、今では200ではなく50の比較を行っています(比較がより複雑であると想像してください)。

ただし、一般に手動ループの展開は、主に歴史の成果物です。これは、優れたコンパイラーが重要な場合に役立つことの増え続けるリストの1つです。例えば、ほとんどの人が書くこと気にしないx <<= 1か、x += x代わりにx *= 2。あなたが書くだけx *= 2で、コンパイラーはあなたのためにそれを最適なものに最適化します。

基本的に、コンパイラーを推測する必要性はますます少なくなっています。


1
@マイク困惑したときに最適化を行う場合は確かに最適化をオフにしますが、Poita_が投稿したリンクを読む価値はあります。コンパイラーはそのビジネスでひどく上手くなっています。
dmckee ---元モデレーターの子猫2010

16
@マイク「私はそれらのことをいつ行うべきか、いつすべきでないかを完全に決定することができます」...あなたが超人でない限り、私はそれを疑います。
Boy氏

5
@ジョン:なぜあなたはそれを言うのか分かりません。人々は、最適化はある種の黒人芸術のみのコンパイラであり、良い推測者はその方法を知っていると考えているようです。すべては、命令とサイクル、およびそれらが費やされる理由に帰着します。SOで何度も説明したように、それらがどのように、そしてなぜ使われているのかは簡単にわかります。かなりの時間を費やさなければならないループがあり、ループのオーバーヘッドでコンテンツと比較してあまりにも多くのサイクルを費やしている場合、それを確認して展開できます。コードの巻き上げについても同じです。天才はかかりません。
Mike Dunlavey、2010年

3
それほど難しくはないと思いますが、コンパイラと同じくらい速くできるかどうかはまだ疑問です。とにかくコンパイラがあなたのためにそれをやっている問題は何ですか?気に入らない場合は、最適化をオフにして、1990年のように時間を無駄にしてください。
Boy氏

2
ループのアンロールによるパフォーマンスの向上は、保存している比較とは関係ありません。何もありません。
bobbogo 2015年

14

最新のハードウェアでの分岐予測に関係なく、ほとんどのコンパイラーはとにかくループの展開を行います。

コンパイラーがどれだけ最適化しているかを調べることは価値があります。

Felix von Leitnerのプレゼンテーションは、この主題について非常に啓発的であることがわかりました。ぜひお読みください。概要:最新のコンパイラーはとても賢いので、手の最適化はほとんど効果がありません。


7
それは良い読みですが、私がマークにあると思った唯一の部分は、データ構造を単純に保つことについて彼が話すところでした。残りの部分は正確でしたが、実行されいるものは正しくなければならないという、巨大で明言されいない仮定に基づいています。私が行うチューニングでは、抽象化コードの不要な山に膨大な時間が費やされているときに、レジスターとキャッシュのミスを心配している人を見つけます。
Mike Dunlavey、2010年

3
「手の最適化はほとんど効果がありません」→タスクに完全に慣れていない場合は、おそらく本当です。そうでなければ単に真実ではありません。
Veedrac 2017年

2019でも、コンパイラーの自動試行よりも大幅に改善された手動アンロールを実行しました。コンパイラーにすべてを実行させるのはそれほど信頼できません。それほど頻繁には展開されないようです。少なくともc#では、すべての言語の代わりに話すことはできません。
WDUK、

2

私が理解している限り、最新のコンパイラはすでに適切な場所でループを展開しています-例としてgccがあり、最適化フラグを渡した場合、マニュアルでは次のように述べています。

コンパイル時またはループへの入り口で反復回数を決定できるループを展開します。

そのため、実際には、コンパイラが簡単なケースを実行する可能性があります。したがって、必要な反復回数をコンパイラーが簡単に判別できるように、ループのできるだけ多くを容易にすることは、あなた次第です。


コンパイラーは通常、ループのアンロールを行わないため、ヒューリスティックは非常に高価です。静的コンパイラーは、より多くの時間を費やすことができますが、2つの主要な方法の違いが重要です。
アベル

2

ループの展開は、手動で展開した場合でも、コンパイラーで展開した場合でも、特に最近のx86 CPU(Core 2、Core i7)では逆効果になることがよくあります。結論:このコードを展開する予定のすべてのCPUで、ループアンロールを使用して、または使用せずにコードをベンチマークします。


特にrecet x86 CPUではなぜですか?
JohnTortugo 2013

7
@JohnTortugo:現代のx86 CPUは、小さなループに対して特定の最適化を行っています。たとえば、CoreおよびNehalemアーキテクチャのLoop Stream Detectorを参照してください。ループを展開すると、LSDキャッシュ内に収まるほど小さくならないため、この最適化は無効になります。たとえばtomshardware.com/reviews/Intel-i7-nehalem-cpu,2041-3.htmlを
Paul R

1

知らずに試すことは、それを行う方法ではありません。
この種の処理は全体の時間のかなりの部分を占めますか?

ループのアンロールは、インクリメント/デクリメント、停止条件の比較、およびジャンプのループオーバーヘッドを削減するだけです。ループで実行している処理が、ループのオーバーヘッド自体よりも多くの命令サイクルを必要とする場合、パーセンテージで大幅な改善は見られません。

最大のパフォーマンスを得る方法の例を次に示します。


1

ループの展開は、特定の場合に役立ちます。唯一の利点は、いくつかのテストをスキップしないことです!

たとえば、スカラー置換、ソフトウェアプリフェッチの効率的な挿入が可能になります...積極的に展開すると、実際にそれがどれほど有用であるか(-O3を使用しても、ほとんどのループで10%の速度向上が簡単に得られます)と驚くでしょう。

ただし、前述のとおり、ループに大きく依存し、コンパイラと実験が必要です。ルールを作成するのは難しい(または展開するためのコンパイラのヒューリスティックは完璧になるだろう)


0

ループの展開は問題のサイズに完全に依存します。それはあなたのアルゴリズムがサイズをより小さな作業グループに縮小できるかどうかに完全に依存しています。上記で行ったことは、そのようには見えません。モンテカルロシミュレーションを展開できるかどうかはわかりません。

ループ展開の良いシナリオは、画像を回転することです。別々の作業グループをローテーションできるからです。これを機能させるには、反復回数を減らす必要があります。


シミュレーションのメインループではなく、シミュレーションの内部ループから呼び出されるクイックソートを展開していました。
dsimcha

0

ループ内とループ内の両方にローカル変数が多数ある場合でも、ループのアンロールは役立ちます。ループインデックス用にレジスタを保存する代わりに、これらのレジスタを再利用します。

あなたの例では、レジスタを使いすぎずに、少量のローカル変数を使用します。

(ループ終了への)比較は、比較が重い(つまり、非test命令)場合、特に外部関数に依存する場合にも、大きな欠点です。

ループのアンロールは、分岐予測に対するCPUの認識を高めるのにも役立ちますが、それでもとにかく発生します。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.