この単純なループを考えてみましょう。
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
に置き換えた場合の同じループの制限は479です。これは、gccとclangでも同じです。
更新1
Gcc 7(スナップショット)とclang(トランク)の動作は非常に異なることがわかりました。私が知る限り、clangは960未満のすべての制限に対してループを最適化します。一方、gccは正確な値に敏感であり、上限はありません。たとえば、does制限が200の場合(および他の多くの値)ループを最適化しますが、does制限が202および20002の場合(および他の多くの値)。
デフォルトでは、現在のスナップショットGCC 7は一貫性のない動作をしますが、以前のバージョンでは PARAM_MAX_COMPLETELY_PEEL_TIMES
(16)によるデフォルトの制限があります。コマンドラインからオーバーライドできます。
制限の理由は、あまりにも積極的なループの展開を防ぐことです。これは、 両刃の剣 になります。
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
スルタンのコメントを読んだ後、私はそれを推測する:
ループカウンターが一定の場合(および高すぎない場合)、コンパイラーはループを完全に展開します。
展開されると、コンパイラーは合計操作を1つにグループ化できることを確認します。
ループが何らかの理由で展開されない場合(ここでは、1000
で生成されるステートメントが多すぎる)、操作をグループ化できません。
コンパイラーcould 1000ステートメントのアンロールは1回の追加になりますが、上記のステップ1と2は2つの別々の最適化であるため、アンロールの「リスク」をとることはできません。操作はグループ化できます(例:関数呼び出しはグループ化できません)。
注:これはまれなケースです。ループを使用して同じものを再度追加するのは誰ですか?その場合、コンパイラーが可能なアンロール/最適化に依存しないでください。 1つの命令で適切な操作を直接記述します。