考虑这个简单的循环:
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(trunk)的表现非常不同。据我所知,clang优化了小于960的所有限制的循环。另一方面,gcc对确切值敏感,没有上限。例如,当强度为200(以及许多其他值)时,不优化循环,但当限制为202和20002时, (以及许多其他价值观。)
答案 0 :(得分:88)
默认情况下,当前快照GCC 7的行为不一致,而以前的版本由于PARAM_MAX_COMPLETELY_PEEL_TIMES
而具有默认限制,即16。它可以从命令行覆盖。
限制的基本原理是防止过于激进的循环展开,可以是double-edged sword。
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=n
参数进行调整来调整限制:
max-completely-peeled-insns
完全剥离的环的最大insn数。
max-completely-peel-times
适合完成的循环的最大迭代次数 剥离。
要了解有关insn的更多信息,请参阅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标记循环来评估最终值,它将完全删除它:
#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 :(得分:18)
在阅读Sulthan的评论之后,我想:
如果循环计数器是常量(并且不是太高),编译器会完全展开循环
一旦展开,编译器就会发现总和操作可以分组为一个。
如果循环由于某种原因没有展开(这里:它会用1000
生成太多语句),则无法对操作进行分组。
编译器可以看到1000个语句的展开相当于一次添加,但步骤1&amp;上面描述的图2是两个单独的优化,因此它不能承担&#34;风险&#34;展开,不知道是否可以对操作进行分组(例如:函数调用无法分组)。
注意:这是一个极端情况:谁使用循环再次添加相同的东西?在这种情况下,不要依赖编译器可能的展开/优化;直接在一条指令中写入正确的操作。
答案 2 :(得分:11)
非常好的问题!
在简化代码时,您似乎已经限制了编译器尝试内联的迭代次数或操作次数。正如Grzegorz Szpetkowski所记录的那样,有一些编译器特定的方法可以通过编译指示或命令行选项来调整这些限制。
您还可以使用Godbolt's Compiler Explorer来比较不同的编译器和选项对生成的代码的影响:gcc 6.2
和icc 17
仍然内联960的代码,而clang 3.9
执行不是(使用默认的Godbolt配置,它实际上在73处停止内联)。