为什么在限制为959但不是960时优化一个简单的循环?

ele*_*ora 131 c optimization gcc clang

考虑这个简单的循环:

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

如果您使用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
Run Code Online (Sandbox Code Playgroud)

换句话说,它只是将答案设置为960而不进行循环.

但是,如果您将代码更改为:

float f(float x[]) {
  float p = 1.0;
  for (int i = 0; i < 960; i++)
    p += 1;
  return p;
}
Run Code Online (Sandbox Code Playgroud)

生成的程序集实际执行循环求和?例如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
Run Code Online (Sandbox Code Playgroud)

为什么这个以及为什么clang和gcc完全相同?


因为如果你更换同一回路限floatdouble是479这是gcc和铛一样了.

更新1

事实证明,gcc 7(快照)和clang(trunk)表现得非常不同.据我所知,clang优化了小于960的所有限制的循环.另一方面,gcc对确切值敏感,没有上限.例如,它优化圈外时的极限是200(以及许多其他的值),但它确实当极限是202和20002(以及许多其他的值).

Grz*_*ski 88

TL; DR

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

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

GCC版本<= 6.3.0

GCC的相关优化选项是-fpeel-loops,它与flag一起间接启用-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
Run Code Online (Sandbox Code Playgroud)

消息来自/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;
    }
Run Code Online (Sandbox Code Playgroud)

因此,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
Run Code Online (Sandbox Code Playgroud)

可以通过plaing with max-completely-peeled-insns=nmax-completely-peel-times=nparams 来调整限制:

max-completely-peeled-insns
Run Code Online (Sandbox Code Playgroud)

完全剥离的环的最大insn数.

max-completely-peel-times
Run Code Online (Sandbox Code Playgroud)

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

要了解有关insn的更多信息,请参阅GCC Internals Manual.

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

-march=core-avx2 -Ofast --param max-completely-peeled-insns=1000 --param max-completely-peel-times=1000
Run Code Online (Sandbox Code Playgroud)

然后代码变成:

f:
        vmovss  xmm0, DWORD PTR .LC0[rip]
        ret
.LC0:
        .long   1148207104
Run Code Online (Sandbox Code Playgroud)

我不确定Clang究竟做了什么,以及如何调整其限制,但正如我所观察到的,你可以强制它通过使用unroll pragma标记循环来评估最终值,它将完全删除它:

#pragma unroll
for (int i = 0; i < 960; i++)
    p++;
Run Code Online (Sandbox Code Playgroud)

结果成:

.LCPI0_0:
        .long   1148207104              # float 961
f:                                      # @f
        vmovss  xmm0, dword ptr [rip + .LCPI0_0] # xmm0 = mem[0],zero,zero,zero
        ret
Run Code Online (Sandbox Code Playgroud)

  • 您可能想提及*为什么*GCC故意以这种方式限制自己.具体来说,如果您过于积极地展开循环,则二进制文件会变得更大,并且您不太可能适合L1缓存.相对于保存一些条件跳转,缓存未命中可能[相当昂贵](https://gist.github.com/jboner/2841832),假设分支预测良好(对于典型的循环,您将拥有). (14认同)
  • 你解释了剥离的机制,但没有说明960的相关性是什么,或者为什么甚至有一个限制 (13认同)
  • @MM:GCC 6.3.0 和最新的 snaphost 之间的剥离行为完全不同。对于前者,我强烈怀疑硬编码限制是由“PARAM_MAX_COMPLETELY_PEEL_TIMES”参数强制执行的,该参数在“/gcc/params.def:321”中定义,值为 16。 (2认同)

Jea*_*bre 19

在阅读Sulthan的评论后,我想:

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

  2. 一旦它展开,编译器就会看到sum操作可以分组为一个.

如果循环由于某种原因未展开(此处:它将生成太多语句1000),则无法对操作进行分组.

编译器可以看到1000个语句的展开相当于一次添加,但上面描述的步骤1和2是两个单独的优化,因此它不能承担展开的"风险",不知道是否可以对操作进行分组(例如:函数调用无法分组).

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

  • 有趣的是,如果将`float`更改为`int`,由于其归纳变量优化(`-fivopts`),gcc编译器能够强制减少循环而不管迭代次数.但那些似乎不适用于"浮动". (5认同)

chq*_*lie 12

非常好的问题!

在简化代码时,您似乎已经限制了编译器尝试内联的迭代次数或操作次数.正如Grzegorz Szpetkowski所记录的那样,有一些编译器特定的方法可以通过编译指示或命令行选项来调整这些限制.

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