制限が959ではなく960であるときに単純なループが最適化されるのはなぜですか?


131

次の単純なループについて考えてみます。

float f(float x[]) {
  float p = 1.0;
  for (int i = 0; i < 959; i++)
    p += 1;
  return p;
}

gcc 7(スナップショット)またはclang(トランク)でコンパイルすると、-march=core-avx2 -Ofast非常によく似たものになります。

.LCPI0_0:
        .long   1148190720              # float 960
f:                                      # @f
        vmovss  xmm0, dword ptr [rip + .LCPI0_0] # xmm0 = mem[0],zero,zero,zero
        ret

つまり、ループせずに答えを960に設定するだけです。

ただし、コードを次のように変更した場合:

float f(float x[]) {
  float p = 1.0;
  for (int i = 0; i < 960; i++)
    p += 1;
  return p;
}

生成されたアセンブリは実際にループ合計を実行しますか?たとえば、clangは次のようになります。

.LCPI0_0:
        .long   1065353216              # float 1
.LCPI0_1:
        .long   1086324736              # float 6
f:                                      # @f
        vmovss  xmm0, dword ptr [rip + .LCPI0_0] # xmm0 = mem[0],zero,zero,zero
        vxorps  ymm1, ymm1, ymm1
        mov     eax, 960
        vbroadcastss    ymm2, dword ptr [rip + .LCPI0_1]
        vxorps  ymm3, ymm3, ymm3
        vxorps  ymm4, ymm4, ymm4
.LBB0_1:                                # =>This Inner Loop Header: Depth=1
        vaddps  ymm0, ymm0, ymm2
        vaddps  ymm1, ymm1, ymm2
        vaddps  ymm3, ymm3, ymm2
        vaddps  ymm4, ymm4, ymm2
        add     eax, -192
        jne     .LBB0_1
        vaddps  ymm0, ymm1, ymm0
        vaddps  ymm0, ymm3, ymm0
        vaddps  ymm0, ymm4, ymm0
        vextractf128    xmm1, ymm0, 1
        vaddps  ymm0, ymm0, ymm1
        vpermilpd       xmm1, xmm0, 1   # xmm1 = xmm0[1,0]
        vaddps  ymm0, ymm0, ymm1
        vhaddps ymm0, ymm0, ymm0
        vzeroupper
        ret

これはなぜですか、なぜclangとgccでまったく同じですか?


同じループの限界あなたが交換した場合floatには、doubleこれが再びgccと打ち鳴らすために同じである479です。

アップデート1

gcc 7(スナップショット)とclang(トランク)の動作は大きく異なります。clangは、960未満のすべての制限についてループを最適化します。一方、gccは正確な値の影響を受けやすく、上限はありません。例えば、それはしない制限が200(ならびに多くの他の値)である場合、ループを最適化するが、それはありません制限は202と20002(ならびに多くの他の値)である場合。


3
Sulthanがおそらく意味することは、1)コンパイラがループをアンロールし、2)アンロールされると、合計演算が1つにグループ化できることがわかるということです。ループを展開しないと、操作をグループ化できません。
ジャン=フランソワ・ファーブル

3
奇数のループがあると、展開がより複雑になります。最後の数回の反復は特別に行う必要があります。これで、オプティマイザがショートカットを認識できなくなるモードにぶつかるのに十分かもしれません。それはかなり可能性が高いです、それは最初に特別な場合のためにコードを追加しなければならず、それからそれを再び取り除く必要があるでしょう。耳の間でオプティマイザを使用することは常に最善です:)
Hans Passant 2017

3
また959よりも小さい任意の数のために最適化され@HansPassant
エレオノーラ

6
これは通常、非常識な量を展開するのではなく、誘導変数の除去で行われませんか?959倍のアンロールはおかしいです。
ハロルド2017

4
@eleanoraそのコンパイルエクスプローラで遊んだところ、次の条件が満たされているようです(gccスナップショットについてのみ話しています)。ループカウントが4の倍数で少なくとも72の場合、ループはアンロールされません(または、 4の係数); それ以外の場合は、全体のループは、一定で置き換えられる-ループカウントが2000000001.私の疑いであったとしても:時期尚早の最適化は、同様に(時期尚早「4のちょっと、複数のそのアンロールのための良い」対Aそのブロックさらなる最適化をより徹底的な「とにかくこのループの扱いは何ですか?」)
ハーゲン・フォン・アイツェン

回答:


88

TL; DR

デフォルトでは、現在のスナップショットGCC 7は一貫性のない動作をしますが、以前のバージョンPARAM_MAX_COMPLETELY_PEEL_TIMESには、16 によるデフォルトの制限があります。これは、コマンドラインからオーバーライドできます。

制限の理論的根拠は、あまりにも積極的なループの展開を防ぐことです。これは、両刃の剣になる可能性があります。

GCCバージョン<= 6.3.0

GCCに関連する最適化オプションは-fpeel-loopsであり、これはフラグと一緒に間接的に有効になります-Ofast(強調は私のものです):

(プロファイルフィードバックまたは静的分析から)あまりロールしない十分な情報があるピールループ。また、完全なループピーリングをオンにします(つまり、一定の反復回数が少ないループを完全に削除します)。

-O3またはで有効化されてい-fprofile-useます。

詳細を追加するには、以下を追加し-fdump-tree-cunrollます。

$ head test.c.151t.cunroll 

;; Function f (f, funcdef_no=0, decl_uid=1919, cgraph_uid=0, symbol_order=0)

Not peeling: upper bound is known so can unroll completely

メッセージは/gcc/tree-ssa-loop-ivcanon.c次のとおりです。

if (maxiter >= 0 && maxiter <= npeel)
    {
      if (dump_file)
        fprintf (dump_file, "Not peeling: upper bound is known so can "
         "unroll completely\n");
      return false;
    }

したがって、try_peel_loop関数はを返しますfalse

より詳細な出力には、次のようにして到達できます-fdump-tree-cunroll-details

Loop 1 iterates 959 times.
Loop 1 iterates at most 959 times.
Not unrolling loop 1 (--param max-completely-peeled-times limit reached).
Not peeling: upper bound is known so can unroll completely

max-completely-peeled-insns=nおよびmax-completely-peel-times=nparamsを使用して制限を調整することができます。

max-completely-peeled-insns

完全にピールされたループの最大インス数。

max-completely-peel-times

完全なピーリングに適したループの最大反復回数。

insnsの詳細については、GCC Internals Manualを参照してください。

たとえば、次のオプションでコンパイルしたとします。

-march=core-avx2 -Ofast --param max-completely-peeled-insns=1000 --param max-completely-peel-times=1000

その後、コードは次のようになります。

f:
        vmovss  xmm0, DWORD PTR .LC0[rip]
        ret
.LC0:
        .long   1148207104

クラン

Clangが実際に何をしてどのようにその制限を微調整するかはわかりませんが、観察したように、ループをunrollプラグマでマークすることで強制的に最終値を評価することができ、完全に削除されます。

#pragma unroll
for (int i = 0; i < 960; i++)
    p++;

結果は:

.LCPI0_0:
        .long   1148207104              # float 961
f:                                      # @f
        vmovss  xmm0, dword ptr [rip + .LCPI0_0] # xmm0 = mem[0],zero,zero,zero
        ret

この非常に素晴らしい答えをありがとう。他の人が指摘したように、gccは正確な制限サイズに敏感なようです。たとえば、912 godbolt.org/g/EQJHvTのループを排除できません。その場合、fdump-tree-cunroll-detailsは何と言っていますか?
eleanora 2017

実際には200でもこの問題があります。これはすべて、godboltが提供するgcc 7のスナップショットにあります。godbolt.org/g/Vg3SVs これは、clangにはまったく適用されません。
eleanora 2017

13
剥離のメカニズムについて説明しますが、960の関連性や、制限がある理由については説明しません
MM

1
@MM:ピーリング動作は、GCC 6.3.0と最新のスナップホストでは完全に異なります。前者の場合には、私は強く、ハードコードされた制限をすることにより実施されることを、疑いPARAM_MAX_COMPLETELY_PEEL_TIMESで定義されているPARAM、/gcc/params.def:321値16と
グジェゴシSzpetkowski

14
なぜ GCCがこのように意図的に制限するのについて言及したいと思うかもしれません。具体的には、ループをあまりにも積極的に展開すると、バイナリが大きくなり、L1キャッシュに収まる可能性が低くなります。キャッシュミスは、適切な分岐予測(通常のループの場合)を想定しているため、いくつかの条件付きジャンプを保存する場合に比べてかなり高価になる可能性があります。
ケビン

19

スルタンのコメントを読んだ後、私はそれを推測します:

  1. ループカウンターが一定(かつ高すぎない)の場合、コンパイラーはループを完全に展開します

  2. 展開されると、コンパイラーは和演算を1つにグループ化できることを確認します。

ループが何らかの理由で展開されない場合(ここでは、で生成されるステートメントが多すぎるため1000)、操作をグループ化できません。

コンパイラー、1000ステートメントのアンロールが1回の追加に相当することを認識できますが、上記のステップ1と2は2つの別々の最適化であるため、アンロールの「リスク」をとることができず、操作をグループ化できるかどうかがわかりません(例:関数呼び出しはグループ化できません)。

注:これはコーナーケースです:ループを使用して同じものを繰り返し追加するのは誰ですか?その場合は、コンパイラーの可能な展開/最適化に依存しないでください。1つの命令で適切な操作を直接記述します。


1
次に、そのnot too high部分に集中できますか?なぜリスクが存在しないの100ですか?私は何かを推測しました...上記の私のコメントで..それはその理由かもしれませんか?
user2736738 2017

コンパイラーは、トリガーされる可能性のある浮動小数点の不正確さを認識していないと思います。命令サイズの制限にすぎないと思います。あなたとmax-unrolled-insns一緒にmax-unrolled-times
ジャン=フランソワ・ファーブル

ああ、それは私の考えや推測のようなものでした...より明確な推論を得たいです。
user2736738 2017

5
興味深いことに、をに変更するfloatint、gccコンパイラーは、誘導変数の最適化(-fivopts)により、反復回数に関係なくループを強度削減できます。しかし、それらはfloatsで動作しないようです。
Tavian Barnes、2017

1
@CortAmmon正解です。GCCがMPFRを使用して非常に大きな数値を正確に計算しているため、エラーや精度の損失が累積する同等の浮動小数点演算とは異なる結果が出ることに驚いて動揺した人を読んだことを思い出します。多くの人が間違った方法で浮動小数点を計算することを示しに行きます。
Zan Lynx

12

とても良い質問です!

コードを簡略化するときにコンパイラーがインライン化しようとする反復または操作の数の制限に達したようです。Grzegorz Szpetkowskiによって文書化されているように、プラグマまたはコマンドラインオプションでこれらの制限を微調整するコンパイラ固有の方法があります。

Godboltのコンパイラエクスプローラーで遊ぶこともできます:異なるコンパイラやオプションは、生成されたコードに与える影響を比較することgcc 6.2icc 17のに対し、960のインラインまだコードをclang 3.9(デフォルトGodboltの設定、それは実際には73でインライン化を停止して)いません。


質問を編集して、使用していたgccとclangのバージョンを明確にしました。godbolt.org/g/FfwWjLを参照してください。たとえば-Ofastを使用しています。
eleanora 2017
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.