为啥限制为 959 而不是 960 时优化简单循环?
Posted
技术标签:
【中文标题】为啥限制为 959 而不是 960 时优化简单循环?【英文标题】:Why is a simple loop optimized when the limit is 959 but not 960?为什么限制为 959 而不是 960 时优化简单循环? 【发布时间】:2017-06-28 19:20:54 【问题描述】:考虑这个简单的循环:
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 (snapshot) 和 clang (trunk) 的行为非常不同。据我所知,clang 优化了小于 960 的所有限制的循环。另一方面,gcc 对精确值很敏感,并且没有上限。例如,当限制为 200(以及许多其他值)时,它不优化循环,但当限制为 202 和 20002(以及许多其他值)。
【问题讨论】:
Sulthan 可能的意思是 1) 编译器展开循环,2) 一旦展开,就会发现求和运算可以归为一个。如果循环未展开,则无法对操作进行分组。 奇数个循环会使展开更加复杂,最后几次迭代必须特别完成。这可能足以使优化器进入无法识别快捷方式的模式。很可能,它首先必须为特殊情况添加代码,然后必须再次将其删除。在耳朵之间使用优化器总是最好的:) @HansPassant 它还针对任何小于 959 的数字进行了优化。 这通常不是通过归纳变量消除来完成,而不是展开一个疯狂的数量吗?展开 959 倍是疯狂的。 @eleanora 我用那个编译资源管理器玩过,以下似乎成立(仅谈论 gcc 快照):如果循环计数是 4 的倍数并且至少是 72,那么循环是 不展开(或者更确切地说,展开4倍);否则,整个循环将被一个常量替换 - 即使循环计数是 2000000001。我的怀疑:过早优化(如过早的“嘿,4 的倍数,这有利于展开”阻止进一步优化与更彻底的“这个循环到底是怎么回事?”) 【参考方案1】:TL;DR
默认情况下,当前快照 GCC 7 的行为不一致,而以前的版本由于 PARAM_MAX_COMPLETELY_PEEL_TIMES
而具有默认限制,即 16。可以从命令行覆盖它。
限制的基本原理是防止过于激进的循环展开,可以是double-edged sword。
GCC 版本
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
适合完成的循环的最大迭代次数 剥落。
了解更多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 标记循环来强制它评估最终值,它会完全删除它:
#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 说明了什么? 其实连200都有这个问题。这一切都在 Godbolt 提供的 gcc 7 的快照中。 godbolt.org/g/Vg3SVs这根本不适用于clang。 你解释了剥离的机制,但没有解释 960 的相关性是什么,或者为什么根本没有限制 @M.M:GCC 6.3.0 和最新的 snaphost 之间的剥离行为完全不同。对于前者,我强烈怀疑硬编码限制由PARAM_MAX_COMPLETELY_PEEL_TIMES
参数强制执行,该参数在/gcc/params.def:321
中定义,值为16。
您可能想提一下为什么 GCC 故意以这种方式限制自己。具体来说,如果您过于激进地展开循环,则二进制文件会变得更大,并且您不太可能适合 L1 缓存。缓存未命中可能是quite expensive,相对于保存一些条件跳转,假设有良好的分支预测(对于典型的循环,您将拥有)。【参考方案2】:
看了苏尔坦的评论,我猜:
如果循环计数器为常数(并且不太高),编译器将完全展开循环
展开后,编译器会看到求和运算可以归为一个。
如果循环由于某种原因没有展开(这里:它会生成太多带有1000
的语句),则无法对操作进行分组。
编译器可以看到 1000 条语句的展开相当于一次加法,但是上面描述的第 1 步和第 2 步是两个单独的优化,所以它不能冒展开的“风险”,而不是知道操作是否可以分组(例如:函数调用不能分组)。
注意:这是一个极端情况:谁使用循环再次添加相同的东西?在这种情况下,不要依赖编译器可能的展开/优化;直接在一条指令中编写正确的操作。
【讨论】:
那么您能专注于not too high
部分吗?我的意思是为什么在 100
的情况下不存在风险?我猜到了……在我上面的评论中……这可能是原因吗?
我认为编译器没有意识到它可能触发的浮点不准确性。我想这只是指令大小的限制。你有max-unrolled-insns
和max-unrolled-times
啊,这是我的想法或猜测……希望得到更清晰的推理。
有趣的是,如果您将float
更改为int
,gcc 编译器能够在不考虑迭代次数的情况下减少循环,因为它的归纳变量优化 (-fivopts
)。但这些似乎不适用于float
s。
@CortAmmon 是的,我记得读过一些人对 GCC 使用 MPFR 来精确计算非常大的数字感到惊讶和不安,这与会累积错误和精度损失。表明许多人以错误的方式计算浮点数。【参考方案3】:
非常好的问题!
在简化代码时,编译器尝试内联的迭代或操作的数量似乎已达到限制。正如 Grzegorz Szpetkowski 所记录的,有编译器特定的方法可以使用编译指示或命令行选项调整这些限制。
您还可以使用Godbolt's Compiler Explorer 来比较不同的编译器和选项如何影响生成的代码:gcc 6.2
和 icc 17
仍然内联 960 的代码,而 clang 3.9
没有(使用默认的 Godbolt 配置,它实际上在 73 处停止内联)。
【讨论】:
我已经编辑了这个问题,以明确我正在使用的 gcc 和 clang 的版本。见godbolt.org/g/FfwWjL。例如,我正在使用 -Ofast。以上是关于为啥限制为 959 而不是 960 时优化简单循环?的主要内容,如果未能解决你的问题,请参考以下文章
为啥 C 和 C++ for 循环使用 int 而不是 unsigned int?
当循环在异步函数内部而不是相反时,为啥 Async/Await 可以正常工作?