1. ホーム
  2. c

[解決済み] 単純なループが最適化されるのは、なぜですか?

2022-07-06 06:37:50

質問

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

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

gcc 7 (snapshot)またはclang (trunk)でコンパイルする際に -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 (snapshot) と clang (trunk) では挙動が大きく異なることがわかりました。clang は私が知る限り、960以下のすべての制限に対してループを最適化します。例えば ではありません。 ループを最適化しませんが、(他の多くの値と同様に) はループを最適化します。

どのように解決するのですか?

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=nmax-completely-peel-times=n のパラメータを指定します。

max-completely-peeled-insns

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

max-completely-peel-times

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

insnsについてより詳しく知るには、以下のサイトを参照してください。 GCC内部マニュアル .

例えば、以下のようなオプションでコンパイルした場合。

-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 が実際に何をするのか、どのようにその限界を調整するのかは分かりませんが、私が観察したところ、ループに 展開プラグマ とマークすれば、完全に削除されます。

#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