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

考虑下面这个简单的循环:

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

如果您使用 gcc7(快照)或 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 是完全相同的?


如果用 double替换 float,则相同循环的限制是479。这对于 gcc 和 clang 也是一样的。

更新1

事实证明,gcc7(快照)和 clang (主干)的行为非常不同。叮当声优化了循环的所有限制小于960,我可以告诉。另一方面,gcc 对精确值很敏感,没有上限。例如,当限制为200(以及许多其他值)时,它的 没有优化出循环,但是当限制为202和20002(以及许多其他值)时,它的 是的优化出循环。

7967 次浏览

看了 Sulthan 的评论后,我想:

  1. 如果循环计数器是常量(且不太高) ,编译器将完全展开循环

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

如果由于某种原因没有展开循环(在这里: 使用 1000会生成太多语句) ,则不能对操作进行分组。

编译器 可以发现1000条语句的展开只是一个附加操作,但是上面描述的步骤1和步骤2是两个独立的优化,所以它不能冒展开操作的“风险”,因为它不知道操作是否可以分组(例如: 函数调用不能分组)。

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

DR

默认情况下,当前快照 GCC7的行为不一致,而以前的版本由于 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

完全剥离的循环的最大 inns 数。

max-completely-peel-times

循环的最大迭代次数 剥皮。

要了解更多关于 inns 的信息,可以参考 海湾合作委员会内部手册

例如,如果使用以下选项编译:

-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

问得好!

在简化代码时,似乎遇到了编译器试图内联的迭代或操作数量的限制。正如 Grzegorz Szpetkowski 所记录的,有一些编译器特定的方法可以用杂注或命令行选项来调整这些限制。

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