web-dev-qa-db-ja.com

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

この単純なループを考えてみましょう。

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でまったく同じなのか?


floatdoubleに置き換えた場合の同じループの制限は479です。これは、gccとclangでも同じです。

更新1

Gcc 7(スナップショット)とclang(トランク)の動作は非常に異なることがわかりました。私が知る限り、clangは960未満のすべての制限に対してループを最適化します。一方、gccは正確な値に敏感であり、上限はありません。たとえば、does制限が200の場合(および他の多くの値)ループを最適化しますが、does制限が202および20002の場合(および他の多くの値)。

131
eleanora

TL; DR

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

制限の理由は、あまりにも積極的なループの展開を防ぐことです。これは、 両刃の剣 になります。

GCCバージョン<= 6.3.0

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

(プロファイルフィードバックまたはstatic analysisから)あまりロールしないほど十分な情報があるピールループ。また、完全なループピーリングも有効にします(つまり、小さな一定の反復回数でループを完全に削除します)。

-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=n paramsを設定することにより、制限を微調整することができます。

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が実際に何をし、どのように制限を調整するかはわかりませんが、先ほど見たように、ループを nroll pragma でマークすることで強制的に最終値を評価することができ、完全に削除されます:

#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
89

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

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

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

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

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

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

19