为什么是一个简单的循环优化时,极限是959而不是960?

考虑这个简单的循环:

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

如果使用-march=core-avx2 -Ofast编译gcc 7(快照)或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; } 

生成的程序集实际执行循环和? 例如铛给出:

 .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完全一样?


如果用doublereplacefloat ,则相同循环的限制为479.这与gcc和clang相同。

更新1

事实certificate,海湾合作委员会7(快照)和铛(干线)行为非常不同。 据我所知,铛可以优化所有小于960的极限环。 海湾合作委员会另一方面是敏感的确切的价值,并没有一个上限。 例如,当限制为200(以及许多其他值)时,它不会优化循环,但当限制为202和20002(以及其他许多值)时,它不会进行优化。

TL; DR

默认情况下,当前快照GCC 7行为不一致,而以前的版本由于PARAM_MAX_COMPLETELY_PEEL_TIMES (16)而具有默认限制。它可以从命令行覆盖。

限制的基本原理是防止过于激进的循环展开,这可能是一把双刃剑 。

GCC版本<= 6.3.0

GCC的相关优化选项是-fpeel-loops ,它是与-Ofast (强调是我的)标志间接启用的:

剥离循环,有足够的信息,他们不会滚动太多(从configuration文件反馈或静态分析 )。 它也打开完整的循环剥离(即,以小的恒定迭代次数完全去除循环 )。

启用-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 

一个完全去皮的循环insins的最大数量。

 max-completely-peel-times 

循环的最大迭代次数,适合完全剥离。

要了解有关insn的更多信息,请参阅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实际上做了什么以及如何调整它的极限,但正如我所观察到的,您可以强制它通过使用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. 如果循环计数器是恒定的(而不是太高),编译器完全展开循环,

  2. 一旦展开,编译器就会看到总和操作可以分组到一个中。

如果循环由于某种原因而未展开(这里:它会生成1000过多的语句),操作无法分组。

编译器可以看到1000条语句的展开等于一个单独的添加,但是上面描述的步骤1和2是两个单独的优化,所以它不能承担展开的“风险”,不知道操作是否可以分组(例如:函数调用不能被分组)。

注意:这是一个特例:谁使用循环再次添加相同的东西? 在这种情况下,不要依赖编译器可能的展开/优化; 直接在一条指令中写入正确的操作。

非常好的问题!

在简化代码时,您似乎已经达到了编译器尝试内联的迭代次数或操作次数的限制。 正如Grzegorz Szpetkowski所述,编译器具体的方法是通过编译指令或命令行选项来调整这些限制。

你也可以使用Godbolt的编译器资源pipe理器来比较不同的编译器和选项如何影响代码生成: gcc 6.2icc 17仍然内联960的代码,而clang 3.9不(默认的Godboltconfiguration,它实际上停止在73 )。